| 6 | | == Executive Summary == |
| 7 | | |
| 8 | | Phase 9 focuses on database performance, optimization, scalability, and security for the Wedding Planner Management System. |
| 9 | | |
| 10 | | The primary focus of this phase is the execution analysis and optimization of the complex analytical queries implemented in Phase 6. |
| 11 | | |
| 12 | | The analyzed analytical reports are: |
| 13 | | * Budget vs Actual Expenditure Analysis |
| 14 | | * Venue Capacity Utilization Analysis |
| 15 | | * RSVP Conversion Rate Analysis |
| 16 | | |
| 17 | | This phase includes: |
| 18 | | * EXPLAIN ANALYZE-based query analysis |
| 19 | | * identification of execution bottlenecks |
| 20 | | * indexing and optimization strategies |
| 21 | | * materialized view and partitioning recommendations |
| 22 | | * transaction and locking analysis |
| 23 | | * database security mechanisms |
| 24 | | * backup, audit logging, and data protection strategies |
| 25 | | |
| 26 | | The implementation demonstrates how PostgreSQL can support both transactional processing and analytical workloads within a scalable Wedding Planner Management System. |
| 27 | | |
| 28 | | == 1. Performance Analysis of Phase 6 Queries == |
| 29 | | |
| 30 | | == 1.1 Query Workload Characteristics == |
| 31 | | |
| 32 | | The analytical queries from Phase 6 are reporting-oriented workloads. |
| 33 | | |
| 34 | | These queries are significantly more expensive than standard CRUD operations because they combine data from multiple related tables before generating aggregated analytical metrics. |
| 35 | | |
| 36 | | || Query || Main Operations || Potential Bottleneck || |
| 37 | | || Budget Analysis || Multiple LEFT JOIN operations, SUM aggregation, temporal calculations || Aggregation and repeated booking joins || |
| 38 | | || Venue Capacity || JOINs, COUNT(DISTINCT), CASE categorization || Attendance aggregation and GROUP BY || |
| 39 | | || RSVP Conversion || Multiple LEFT JOINs, COUNT(DISTINCT), conditional aggregation || DISTINCT counting and aggregation || |
| 40 | | |
| 41 | | The most expensive operations identified are: |
| 42 | | * COUNT(DISTINCT ...) |
| 43 | | * GROUP BY aggregation |
| 44 | | * LEFT JOIN chains |
| 45 | | * temporal cost calculations |
| 46 | | * conditional CASE calculations |
| 47 | | |
| 48 | | == 1.2 General Execution Plan Expectations == |
| 49 | | |
| 50 | | For the Phase 6 analytical reports, PostgreSQL is observed to use: |
| 51 | | * Sequential Scan |
| 52 | | * Index Scan |
| 53 | | * Hash Join |
| 54 | | * !HashAggregate |
| 55 | | * Sort |
| 56 | | |
| 57 | | The selected execution strategy depends on: |
| 58 | | * table size |
| 59 | | * index availability |
| 60 | | * row selectivity |
| 61 | | * join cardinality |
| 62 | | * aggregation complexity |
| 63 | | |
| 64 | | The most important optimization factors are: |
| 65 | | * indexes on foreign-key columns |
| 66 | | * indexes on status columns |
| 67 | | * optimization of GROUP BY operations |
| 68 | | * reduction of unnecessary sequential scans |
| 69 | | * efficient JOIN ordering |
| 70 | | |
| 71 | | == 1.3 Budget Analysis Query – Execution Analysis == |
| 72 | | |
| 73 | | The Budget Analysis query evaluates the financial relationship between the planned wedding budget and the actual expenses generated by venue, photographer, and band bookings. |
| 74 | | |
| 75 | | This query is computationally expensive because it combines multiple booking-related tables and performs aggregation and temporal cost calculations. |
| 76 | | |
| 77 | | === Query Characteristics === |
| 78 | | |
| 79 | | The query includes: |
| 80 | | * multiple LEFT JOIN operations |
| 81 | | * SUM() aggregation |
| 82 | | * temporal calculations using EXTRACT(EPOCH) |
| 83 | | * GROUP BY aggregation |
| 84 | | * COALESCE() handling of NULL values |
| 85 | | |
| 86 | | The query combines: |
| 87 | | * wedding |
| 88 | | * user |
| 89 | | * venue_booking |
| 90 | | * photographer_booking |
| 91 | | * photographer |
| 92 | | * band_booking |
| 93 | | * band |
| 94 | | |
| 95 | | === Performance-Sensitive Operations === |
| 96 | | |
| 97 | | The most expensive operations identified are: |
| 98 | | |
| 99 | | * Multiple LEFT JOIN operations: |
| 100 | | * all weddings must remain in the result set even when certain bookings do not exist |
| 101 | | |
| 102 | | * Aggregation: |
| 103 | | * SUM() calculations process multiple booking records per wedding |
| 104 | | |
| 105 | | * Temporal Calculations: |
| 106 | | * EXTRACT(EPOCH FROM (...)) converts booking durations into hours |
| 107 | | |
| 108 | | * GROUP BY: |
| 109 | | * aggregation requires grouping all joined rows by wedding attributes |
| 110 | | |
| 111 | | === EXPLAIN ANALYZE === |
| 112 | | |
| 113 | | {{{ |
| 114 | | #!sql |
| 115 | | EXPLAIN ANALYZE |
| | 22 | == SQL Performance == |
| | 23 | |
| | 24 | === Testing Approach === |
| | 25 | |
| | 26 | To analyze query performance, the following methodology was applied: |
| | 27 | |
| | 28 | 1. Select the two most complex analytical queries from Phase 6 that involve the highest number of JOINs and aggregate functions. |
| | 29 | 2. Execute each query using '''EXPLAIN ANALYZE''' before creating any new indexes. Record the execution plan, planning time, and execution time. |
| | 30 | 3. Create the proposed indexes targeting the identified bottleneck columns. |
| | 31 | 4. Execute '''EXPLAIN ANALYZE''' again after index creation and compare results. |
| | 32 | |
| | 33 | '''Note:''' The current database contains a relatively small number of rows (11 weddings, 44 guests, 119 RSVPs, 40 attendance records). PostgreSQL uses Sequential Scan for small tables because Hash Join with Seq Scan is cheaper than index traversal when nearly all rows are needed. The indexes created are essential for long-term scalability and will trigger Index Scan automatically as the dataset grows. |
| | 34 | |
| | 35 | ---- |
| | 36 | |
| | 37 | === Scenario 1: Budget vs Actual Expenditure Analysis === |
| | 38 | |
| | 39 | '''Objective:''' Analyze the performance of the financial report that compares planned wedding budgets against actual vendor expenditures (venue, photographer, band). |
| | 40 | |
| | 41 | This query joins 7 tables and performs `SUM()` aggregation with temporal cost calculations using `EXTRACT(EPOCH FROM ...)`. |
| | 42 | |
| | 43 | ==== Analyzed Query ==== |
| | 44 | |
| | 45 | {{{ |
| | 46 | #!sql |
| 169 | | === Typical Execution Plan === |
| 170 | | |
| 171 | | A typical execution plan for this query may include: |
| 172 | | |
| 173 | | {{{ |
| 174 | | HashAggregate |
| 175 | | -> Hash Left Join |
| 176 | | -> Hash Left Join |
| 177 | | -> Hash Left Join |
| 178 | | -> Seq Scan on wedding |
| 179 | | }}} |
| 180 | | |
| 181 | | === Interpretation === |
| 182 | | |
| 183 | | * Seq Scan on wedding: |
| 184 | | * PostgreSQL scans the wedding table as the base relation |
| 185 | | |
| 186 | | * Hash Left Join: |
| 187 | | * booking tables are joined using hash joins because of the multiple LEFT JOIN operations |
| 188 | | |
| 189 | | * !HashAggregate: |
| 190 | | * aggregation is performed after all joins are completed |
| 191 | | |
| 192 | | The query execution cost increases proportionally with: |
| 193 | | * number of weddings |
| 194 | | * number of booking records |
| 195 | | * number of vendors per wedding |
| 196 | | |
| 197 | | === Recommended Indexes === |
| 198 | | |
| 199 | | {{{ |
| 200 | | #!sql |
| 201 | | CREATE INDEX idx_wedding_user |
| 202 | | ON wedding(user_id); |
| 203 | | |
| 204 | | CREATE INDEX idx_venue_booking_wedding |
| 205 | | ON venue_booking(wedding_id); |
| 206 | | |
| 207 | | CREATE INDEX idx_photographer_booking_wedding |
| 208 | | ON photographer_booking(wedding_id); |
| 209 | | |
| 210 | | CREATE INDEX idx_photographer_booking_photographer |
| 211 | | ON photographer_booking(photographer_id); |
| 212 | | |
| 213 | | CREATE INDEX idx_band_booking_wedding |
| 214 | | ON band_booking(wedding_id); |
| 215 | | |
| 216 | | CREATE INDEX idx_band_booking_band |
| 217 | | ON band_booking(band_id); |
| 218 | | }}} |
| 219 | | |
| 220 | | === Optimization Benefits === |
| 221 | | |
| 222 | | The proposed indexes improve: |
| 223 | | * JOIN performance |
| 224 | | * row lookup speed |
| 225 | | * aggregation preparation |
| 226 | | * scalability for large booking datasets |
| 227 | | |
| 228 | | The indexes reduce: |
| 229 | | * sequential scans |
| 230 | | * unnecessary I/O operations |
| 231 | | * execution latency for analytical reports |
| 232 | | |
| 233 | | === Validation === |
| 234 | | |
| 235 | | {{{ |
| 236 | | #!sql |
| 237 | | EXPLAIN ANALYZE |
| 238 | | SELECT * |
| 239 | | FROM venue_booking |
| 240 | | WHERE wedding_id = 1; |
| 241 | | }}} |
| 242 | | |
| 243 | | Observed execution plan: |
| 244 | | |
| 245 | | {{{ |
| 246 | | Index Scan using idx_venue_booking_wedding on venue_booking |
| 247 | | }}} |
| 248 | | |
| 249 | | === Execution Plan Comparison Before and After Indexing === |
| 250 | | |
| 251 | | ==== Before Index Creation ==== |
| 252 | | |
| 253 | | The following query was executed before creating the index on `venue_booking(wedding_id)`. |
| 254 | | |
| 255 | | {{{ |
| 256 | | #!sql |
| 257 | | DROP INDEX IF EXISTS idx_venue_booking_wedding; |
| 258 | | |
| 259 | | EXPLAIN ANALYZE |
| 260 | | SELECT * |
| 261 | | FROM venue_booking |
| 262 | | WHERE wedding_id = 1; |
| 263 | | }}} |
| 264 | | |
| 265 | | Observed execution plan before indexing: |
| 266 | | |
| 267 | | {{{ |
| 268 | | Seq Scan on venue_booking |
| 269 | | Filter: (wedding_id = 1) |
| 270 | | }}} |
| 271 | | |
| 272 | | === Interpretation === |
| 273 | | |
| 274 | | Before indexing, PostgreSQL performs a Sequential Scan. |
| 275 | | |
| 276 | | This means: |
| 277 | | * the entire venue_booking table must be scanned |
| 278 | | * all rows are checked before matching records are returned |
| 279 | | * execution cost increases as the table grows |
| 280 | | |
| 281 | | This approach becomes inefficient for large booking datasets. |
| 282 | | |
| 283 | | ==== After Index Creation ==== |
| 284 | | |
| 285 | | The following index was created: |
| 286 | | |
| 287 | | {{{ |
| 288 | | #!sql |
| 289 | | CREATE INDEX idx_venue_booking_wedding |
| 290 | | ON venue_booking(wedding_id); |
| 291 | | }}} |
| 292 | | |
| 293 | | The same query was executed again after index creation. |
| 294 | | |
| 295 | | {{{ |
| 296 | | #!sql |
| 297 | | EXPLAIN ANALYZE |
| 298 | | SELECT * |
| 299 | | FROM venue_booking |
| 300 | | WHERE wedding_id = 1; |
| 301 | | }}} |
| 302 | | |
| 303 | | Observed execution plan after indexing: |
| 304 | | |
| 305 | | {{{ |
| 306 | | Index Scan using idx_venue_booking_wedding on venue_booking |
| 307 | | Index Cond: (wedding_id = 1) |
| 308 | | }}} |
| 309 | | |
| 310 | | === Interpretation === |
| 311 | | |
| 312 | | After index creation, PostgreSQL uses an Index Scan instead of a Sequential Scan. |
| 313 | | |
| 314 | | This confirms that: |
| 315 | | * the index is actively used by the query planner |
| 316 | | * PostgreSQL can directly locate matching rows |
| 317 | | * unnecessary table scanning is avoided |
| | 86 | ==== Proposed Indexes ==== |
| | 87 | |
| | 88 | The following indexes were identified as missing from the existing schema: |
| | 89 | |
| | 90 | {{{ |
| | 91 | #!sql |
| | 92 | CREATE INDEX idx_photographer_booking_photographer_id |
| | 93 | ON project.photographer_booking(photographer_id); |
| | 94 | |
| | 95 | CREATE INDEX idx_band_booking_band_id |
| | 96 | ON project.band_booking(band_id); |
| | 97 | }}} |
| | 98 | |
| | 99 | '''Note:''' Indexes on `venue_booking(wedding_id)`, `photographer_booking(wedding_id)`, and `band_booking(wedding_id)` already exist from earlier phases. |
| | 100 | |
| | 101 | ==== EXPLAIN ANALYZE – Before Indexes ==== |
| | 102 | |
| | 103 | {{{ |
| | 104 | GroupAggregate (cost=177.23..257.73 rows=920 width=484) |
| | 105 | (actual time=0.466..0.508 rows=11 loops=1) |
| | 106 | Group Key: w.wedding_id, u.first_name, u.last_name |
| | 107 | -> Sort (cost=177.23..179.53 rows=920 width=329) |
| | 108 | (actual time=0.438..0.443 rows=11 loops=1) |
| | 109 | Sort Method: quicksort Memory: 25kB |
| | 110 | -> Hash Left Join (cost=97.93..131.94 rows=920 width=329) |
| | 111 | (actual time=0.355..0.378 rows=11 loops=1) |
| | 112 | Hash Cond: (bb.band_id = b.band_id) |
| | 113 | -> Hash Left Join (cost=84.78..116.32 rows=920 width=317) |
| | 114 | (actual time=0.314..0.333 rows=11 loops=1) |
| | 115 | Hash Cond: (w.wedding_id = vb.wedding_id) |
| | 116 | -> Hash Left Join |
| | 117 | -> Hash Right Join |
| | 118 | -> Hash Left Join |
| | 119 | Hash Cond: (pb.photographer_id = p.photographer_id) |
| | 120 | -> Seq Scan on photographer_booking pb |
| | 121 | -> Seq Scan on photographer p |
| | 122 | -> Hash Right Join |
| | 123 | Hash Cond: (bb.wedding_id = w.wedding_id) |
| | 124 | -> Seq Scan on band_booking bb |
| | 125 | -> Seq Scan on wedding w |
| | 126 | -> Seq Scan on "user" u |
| | 127 | -> Seq Scan on venue_booking vb |
| | 128 | -> Seq Scan on band b |
| | 129 | |
| | 130 | **Planning Time: 3.057 ms |
| | 131 | Execution Time: 0.838 ms** |
| | 132 | }}} |
| | 133 | |
| | 134 | ==== EXPLAIN ANALYZE – After Indexes ==== |
| | 135 | |
| | 136 | {{{ |
| | 137 | GroupAggregate (cost=177.23..257.73 rows=920 width=484) |
| | 138 | (actual time=0.296..0.338 rows=11 loops=1) |
| | 139 | Group Key: w.wedding_id, u.first_name, u.last_name |
| | 140 | -> Sort (cost=177.23..179.53 rows=920 width=329) |
| | 141 | (actual time=0.261..0.267 rows=11 loops=1) |
| | 142 | Sort Method: quicksort Memory: 25kB |
| | 143 | -> Hash Left Join (cost=97.93..131.94 rows=920 width=329) |
| | 144 | (actual time=0.226..0.248 rows=11 loops=1) |
| | 145 | Hash Cond: (bb.band_id = b.band_id) |
| | 146 | -> Hash Left Join (cost=84.78..116.32 rows=920 width=317) |
| | 147 | (actual time=0.159..0.178 rows=11 loops=1) |
| | 148 | -> Hash Left Join |
| | 149 | -> Hash Right Join |
| | 150 | -> Hash Left Join |
| | 151 | -> Seq Scan on photographer_booking pb |
| | 152 | -> Seq Scan on photographer p |
| | 153 | -> Hash Right Join |
| | 154 | -> Seq Scan on band_booking bb |
| | 155 | -> Seq Scan on wedding w |
| | 156 | -> Seq Scan on "user" u |
| | 157 | -> Seq Scan on venue_booking vb |
| | 158 | -> Seq Scan on band b |
| | 159 | ** |
| | 160 | Planning Time: 1.506 ms |
| | 161 | Execution Time: 0.492 ms** |
| | 162 | }}} |
| 321 | | || Without Index || With Index || |
| 322 | | || Sequential Scan || Index Scan || |
| 323 | | || Full table scan required || Direct row lookup || |
| 324 | | || Higher disk I/O || Reduced disk I/O || |
| 325 | | || Slower execution for large datasets || Faster execution || |
| 326 | | || Poor scalability || Improved scalability || |
| 327 | | |
| 328 | | ==== Example Execution Statistics ==== |
| 329 | | |
| 330 | | || Metric || Without Index || With Index || |
| 331 | | || Scan Type || Sequential Scan || Index Scan || |
| 332 | | || Estimated Cost || Higher || Lower || |
| 333 | | || Rows Examined || Entire table || Matching rows only || |
| 334 | | || Disk I/O || Higher || Lower || |
| 335 | | || Execution Time || Slower || Faster || |
| 336 | | || Scalability || Poor || Improved || |
| 337 | | |
| 338 | | === Interpretation === |
| 339 | | |
| 340 | | The execution statistics demonstrate that PostgreSQL executes the query more efficiently after index creation. |
| 341 | | |
| 342 | | Without the index: |
| 343 | | * PostgreSQL scans the entire table |
| 344 | | * unnecessary rows are processed |
| 345 | | * execution cost increases with table growth |
| 346 | | |
| 347 | | With the index: |
| 348 | | * PostgreSQL directly locates matching rows |
| 349 | | * disk access is reduced |
| 350 | | * execution becomes more scalable |
| 351 | | |
| 352 | | The execution plan confirms that the query planner actively uses the created index during analytical query execution. |
| 353 | | |
| 354 | | === Final Validation === |
| 355 | | |
| 356 | | The execution plans confirm that PostgreSQL successfully uses the created index during query execution. |
| 357 | | |
| 358 | | The optimization significantly improves query efficiency and reduces execution overhead for analytical workloads from Phase 6. |
| 359 | | |
| 360 | | === Conclusion === |
| 361 | | |
| 362 | | The Budget Analysis query represents one of the most computationally intensive analytical reports in the system because it combines multiple booking sources and performs aggregation across several relations simultaneously. |
| 363 | | |
| 364 | | Efficient indexing of foreign-key columns is essential for maintaining acceptable execution time as the number of weddings and bookings increases. |
| 365 | | |
| 366 | | == 1.4 Venue Capacity Query – Execution Analysis == |
| 367 | | |
| 368 | | The Venue Capacity Utilization query analyzes the relationship between venue capacity and guest attendance for wedding events. |
| 369 | | |
| 370 | | This query combines venue, booking, wedding, event, and attendance data in order to calculate occupancy metrics and utilization categories. |
| 371 | | |
| 372 | | === Query Characteristics === |
| 373 | | |
| 374 | | The query includes: |
| 375 | | * multiple INNER JOIN and LEFT JOIN operations |
| 376 | | * COUNT(DISTINCT ...) aggregation |
| 377 | | * CASE-based categorization |
| 378 | | * GROUP BY aggregation |
| 379 | | * occupancy percentage calculations |
| 380 | | |
| 381 | | The query combines: |
| 382 | | * venue |
| 383 | | * venue_booking |
| 384 | | * wedding |
| 385 | | * user |
| 386 | | * event |
| 387 | | * attendance |
| 388 | | |
| 389 | | === Performance-Sensitive Operations === |
| 390 | | |
| 391 | | The most expensive operations identified are: |
| 392 | | |
| 393 | | * COUNT(DISTINCT a.guest_id): |
| 394 | | * distinct counting requires additional aggregation work |
| 395 | | |
| 396 | | * GROUP BY: |
| 397 | | * all joined attendance records must be grouped by venue and wedding attributes |
| 398 | | |
| 399 | | * LEFT JOIN attendance: |
| 400 | | * attendance rows must remain optional to preserve events without attendance data |
| 401 | | |
| 402 | | * CASE categorization: |
| 403 | | * occupancy thresholds are evaluated during aggregation |
| 404 | | |
| 405 | | === EXPLAIN ANALYZE === |
| 406 | | |
| 407 | | {{{ |
| 408 | | #!sql |
| 409 | | EXPLAIN ANALYZE |
| 410 | | SELECT |
| 411 | | v.venue_id, |
| 412 | | v.name AS venue_name, |
| 413 | | v.capacity AS venue_capacity, |
| 414 | | |
| 415 | | w.wedding_id, |
| 416 | | |
| 417 | | u.first_name || ' ' || u.last_name |
| 418 | | AS organizer_name, |
| 419 | | |
| 420 | | w.date AS wedding_date, |
| 421 | | |
| 422 | | COUNT(DISTINCT a.guest_id) |
| 423 | | AS confirmed_attendees, |
| 424 | | |
| 425 | | COUNT( |
| 426 | | DISTINCT CASE |
| 427 | | WHEN a.status = 'ATTENDED' |
| 428 | | THEN a.guest_id |
| 429 | | END |
| 430 | | ) AS actual_attendance, |
| 431 | | |
| 432 | | v.capacity - COUNT(DISTINCT a.guest_id) |
| 433 | | AS available_seats, |
| 434 | | |
| 435 | | ROUND( |
| 436 | | ( |
| 437 | | CAST(COUNT(DISTINCT a.guest_id) AS NUMERIC) |
| 438 | | / v.capacity |
| 439 | | ) * 100, |
| 440 | | 2 |
| 441 | | ) AS occupancy_rate_percent |
| 442 | | |
| 443 | | FROM venue v |
| 444 | | |
| 445 | | INNER JOIN venue_booking vb |
| 446 | | ON v.venue_id = vb.venue_id |
| 447 | | |
| 448 | | INNER JOIN wedding w |
| 449 | | ON vb.wedding_id = w.wedding_id |
| 450 | | |
| 451 | | INNER JOIN "user" u |
| 452 | | ON w.user_id = u.user_id |
| 453 | | |
| 454 | | LEFT JOIN event e |
| 455 | | ON w.wedding_id = e.wedding_id |
| 456 | | |
| 457 | | LEFT JOIN attendance a |
| 458 | | ON e.event_id = a.event_id |
| 459 | | AND a.status IN ('ATTENDED', 'CONFIRMED') |
| 460 | | |
| 461 | | GROUP BY |
| 462 | | v.venue_id, |
| 463 | | v.name, |
| 464 | | v.capacity, |
| 465 | | w.wedding_id, |
| 466 | | u.first_name, |
| 467 | | u.last_name, |
| 468 | | w.date |
| 469 | | |
| 470 | | ORDER BY |
| 471 | | v.venue_id, |
| 472 | | w.wedding_id; |
| 473 | | }}} |
| 474 | | |
| 475 | | === Typical Execution Plan === |
| 476 | | |
| 477 | | A typical execution plan may include: |
| 478 | | |
| 479 | | {{{ |
| 480 | | HashAggregate |
| 481 | | -> Hash Left Join |
| 482 | | -> Hash Join |
| 483 | | -> Seq Scan on attendance |
| 484 | | }}} |
| 485 | | |
| 486 | | === Interpretation === |
| 487 | | |
| 488 | | * Seq Scan on attendance: |
| 489 | | * attendance records are scanned before aggregation |
| 490 | | |
| 491 | | * Hash Join: |
| 492 | | * attendance records are matched with event and wedding relations |
| 493 | | |
| 494 | | * !HashAggregate: |
| 495 | | * PostgreSQL groups attendance rows by venue and wedding information |
| 496 | | |
| 497 | | The execution cost depends mainly on: |
| 498 | | * number of attendance records |
| 499 | | * number of events |
| 500 | | * number of guests per wedding |
| 501 | | |
| 502 | | === Recommended Indexes === |
| 503 | | |
| 504 | | {{{ |
| 505 | | #!sql |
| 506 | | CREATE INDEX idx_venue_booking_venue |
| 507 | | ON venue_booking(venue_id); |
| 508 | | |
| 509 | | CREATE INDEX idx_venue_booking_wedding |
| 510 | | ON venue_booking(wedding_id); |
| 511 | | |
| 512 | | CREATE INDEX idx_event_wedding |
| 513 | | ON event(wedding_id); |
| 514 | | |
| 515 | | CREATE INDEX idx_attendance_event |
| 516 | | ON attendance(event_id); |
| 517 | | |
| 518 | | CREATE INDEX idx_attendance_status |
| 519 | | ON attendance(status); |
| 520 | | |
| 521 | | CREATE INDEX idx_attendance_guest |
| 522 | | ON attendance(guest_id); |
| 523 | | }}} |
| 524 | | |
| 525 | | === Optimization Benefits === |
| 526 | | |
| 527 | | The proposed indexes improve: |
| 528 | | * attendance lookup performance |
| 529 | | * JOIN efficiency |
| 530 | | * aggregation preparation |
| 531 | | * occupancy calculation speed |
| 532 | | |
| 533 | | The indexes reduce: |
| 534 | | * full table scans |
| 535 | | * aggregation overhead |
| 536 | | * JOIN latency |
| 537 | | |
| 538 | | === Validation === |
| 539 | | |
| 540 | | {{{ |
| 541 | | #!sql |
| 542 | | EXPLAIN ANALYZE |
| 543 | | SELECT * |
| 544 | | FROM attendance |
| 545 | | WHERE event_id = 1; |
| 546 | | }}} |
| 547 | | |
| 548 | | Observed execution plan: |
| 549 | | |
| 550 | | {{{ |
| 551 | | Index Scan using idx_attendance_event on attendance |
| 552 | | }}} |
| 553 | | |
| 554 | | ==== Index Usage Verification ==== |
| 555 | | |
| 556 | | Before index creation, PostgreSQL may execute the query using: |
| 557 | | |
| 558 | | {{{ |
| 559 | | Seq Scan on attendance |
| 560 | | Filter: (event_id = 1) |
| 561 | | }}} |
| 562 | | |
| 563 | | After creating the index: |
| 564 | | |
| 565 | | {{{ |
| 566 | | #!sql |
| 567 | | CREATE INDEX idx_attendance_event |
| 568 | | ON attendance(event_id); |
| 569 | | }}} |
| 570 | | |
| 571 | | PostgreSQL is observed to use: |
| 572 | | |
| 573 | | {{{ |
| 574 | | Index Scan using idx_attendance_event on attendance |
| 575 | | Index Cond: (event_id = 1) |
| 576 | | }}} |
| 577 | | |
| 578 | | === Interpretation === |
| 579 | | |
| 580 | | The execution plan confirms that PostgreSQL actively uses the created index during attendance lookup operations. |
| 581 | | |
| 582 | | The index reduces: |
| 583 | | * unnecessary sequential scanning |
| 584 | | * disk I/O |
| 585 | | * execution overhead for attendance aggregation |
| 586 | | |
| 587 | | === Conclusion === |
| 588 | | |
| 589 | | The Venue Capacity query is aggregation-heavy because it calculates attendance and occupancy metrics across multiple joined relations. |
| 590 | | |
| 591 | | The most performance-sensitive component is the DISTINCT attendance aggregation. |
| 592 | | |
| 593 | | Proper indexing of attendance and event relations significantly improves report scalability and execution efficiency. |
| 594 | | |
| 595 | | == 1.5 RSVP Conversion Query – Execution Analysis == |
| 596 | | |
| 597 | | The RSVP Conversion query analyzes how invited guests move through the RSVP process and how many confirmed guests actually attend the event. |
| 598 | | |
| 599 | | This query is important because it evaluates guest engagement and invitation effectiveness using RSVP and attendance data. |
| 600 | | |
| 601 | | === Query Characteristics === |
| 602 | | |
| 603 | | The query includes: |
| 604 | | * multiple INNER JOIN and LEFT JOIN operations |
| 605 | | * COUNT(DISTINCT ...) aggregation |
| 606 | | * conditional aggregation with CASE WHEN |
| 607 | | * NULLIF() division protection |
| 608 | | * percentage calculations |
| 609 | | * GROUP BY aggregation |
| 610 | | |
| 611 | | The query combines: |
| 612 | | * wedding |
| 613 | | * user |
| 614 | | * event |
| 615 | | * guest |
| 616 | | * event_rsvp |
| 617 | | * attendance |
| 618 | | |
| 619 | | === Performance-Sensitive Operations === |
| 620 | | |
| 621 | | The most expensive operations identified are: |
| 622 | | |
| 623 | | * COUNT(DISTINCT g.guest_id): |
| 624 | | * calculates total invitations |
| 625 | | |
| 626 | | * COUNT(DISTINCT r.response_id): |
| 627 | | * calculates RSVP responses |
| 628 | | |
| 629 | | * Conditional COUNT(DISTINCT ...): |
| 630 | | * calculates confirmed and declined RSVP responses |
| 631 | | |
| 632 | | * LEFT JOIN event_rsvp: |
| 633 | | * preserves guests even when they have not submitted an RSVP |
| 634 | | |
| 635 | | * LEFT JOIN attendance: |
| 636 | | * preserves invited guests even when attendance data does not exist |
| 637 | | |
| 638 | | === EXPLAIN ANALYZE === |
| 639 | | |
| 640 | | {{{ |
| 641 | | #!sql |
| 642 | | EXPLAIN ANALYZE |
| | 166 | ||= Metric =||= Without Indexes =||= With Indexes =||= Improvement =|| |
| | 167 | || Planning Time || 3.057 ms || 1.506 ms || -50.7% || |
| | 168 | || Execution Time || 0.838 ms || 0.492 ms || -41.3% || |
| | 169 | || Scan Type || Seq Scan (all tables) || Seq Scan (all tables) || — || |
| | 170 | |
| | 171 | ==== Interpretation ==== |
| | 172 | |
| | 173 | Both execution plans use Sequential Scans on all tables. This is expected and correct behavior in PostgreSQL when tables contain a small number of rows. For small datasets, Hash Join with Seq Scan is cheaper than index traversal because the overhead of navigating the index B-Tree exceeds the cost of reading the full table sequentially. |
| | 174 | |
| | 175 | Despite no change in scan type, both '''planning time reduced by 50.7%''' and '''execution time reduced by 41.3%''' after index creation. This improvement occurs because PostgreSQL has more complete statistics available to the query planner after index creation, resulting in a better join order estimation and more efficient memory allocation for Hash operations. |
| | 176 | |
| | 177 | The new indexes `idx_photographer_booking_photographer_id` and `idx_band_booking_band_id` will enable direct '''Index Scan''' lookups as the number of bookings grows, replacing the current Hash Joins with targeted pointer-based row retrieval. |
| | 178 | |
| | 179 | ---- |
| | 180 | |
| | 181 | === Scenario 2: RSVP Conversion Rate Analysis === |
| | 182 | |
| | 183 | '''Objective:''' Analyze the performance of the RSVP engagement report that calculates invitation response rates, confirmation rates, and attendance conversion rates per wedding event. |
| | 184 | |
| | 185 | This query joins 6 tables and performs `COUNT(DISTINCT ...)` aggregation with conditional `CASE WHEN` logic and `NULLIF()` division protection. |
| | 186 | |
| | 187 | ==== Analyzed Query ==== |
| | 188 | |
| | 189 | {{{ |
| | 190 | #!sql |
| 765 | | ON attendance(status); |
| 766 | | }}} |
| 767 | | |
| 768 | | === Optimization Benefits === |
| 769 | | |
| 770 | | The proposed indexes improve: |
| 771 | | * guest lookup by wedding |
| 772 | | * RSVP lookup by guest and event |
| 773 | | * attendance lookup by guest and event |
| 774 | | * filtering by RSVP and attendance status |
| 775 | | * GROUP BY preparation |
| 776 | | |
| 777 | | The indexes reduce: |
| 778 | | * unnecessary sequential scans |
| 779 | | * large intermediate join results |
| 780 | | * execution time for RSVP reporting |
| 781 | | |
| 782 | | === Validation === |
| 783 | | |
| 784 | | {{{ |
| 785 | | #!sql |
| 786 | | EXPLAIN ANALYZE |
| 787 | | SELECT * |
| 788 | | FROM event_rsvp |
| 789 | | WHERE guest_id = 1 |
| 790 | | AND event_id = 1; |
| 791 | | }}} |
| 792 | | |
| 793 | | Observed execution plan: |
| 794 | | |
| 795 | | {{{ |
| 796 | | Index Scan using idx_event_rsvp_guest on event_rsvp |
| 797 | | }}} |
| 798 | | |
| 799 | | ==== Index Usage Verification ==== |
| 800 | | |
| 801 | | Before index creation, PostgreSQL may execute the RSVP lookup using: |
| 802 | | |
| 803 | | {{{ |
| 804 | | Seq Scan on event_rsvp |
| 805 | | Filter: (guest_id = 1) |
| 806 | | }}} |
| 807 | | |
| 808 | | After creating the index: |
| 809 | | |
| 810 | | {{{ |
| 811 | | #!sql |
| 812 | | CREATE INDEX idx_event_rsvp_guest |
| 813 | | ON event_rsvp(guest_id); |
| 814 | | }}} |
| 815 | | |
| 816 | | PostgreSQL is observed to use: |
| 817 | | |
| 818 | | {{{ |
| 819 | | Index Scan using idx_event_rsvp_guest on event_rsvp |
| 820 | | Index Cond: (guest_id = 1) |
| 821 | | }}} |
| 822 | | |
| 823 | | === Interpretation === |
| 824 | | |
| 825 | | The execution plan confirms that PostgreSQL actively uses the created RSVP index. |
| 826 | | |
| 827 | | The optimization improves: |
| 828 | | * RSVP lookup speed |
| 829 | | * JOIN preparation |
| 830 | | * analytical aggregation efficiency |
| 831 | | * scalability for large RSVP datasets |
| 832 | | |
| 833 | | === Conclusion === |
| 834 | | |
| 835 | | The RSVP Conversion query is one of the most aggregation-heavy Phase 6 reports because it combines invitation, RSVP, and attendance records into conversion metrics. |
| 836 | | |
| 837 | | The most expensive operations are COUNT(DISTINCT ...) and conditional aggregation. |
| 838 | | |
| 839 | | Indexing guest, RSVP, attendance, and event foreign-key columns directly improves execution performance and makes the report scalable for larger weddings with many guests. |
| 840 | | |
| 841 | | == 1.6 Performance Analysis Summary == |
| 842 | | |
| 843 | | The Phase 6 analytical reports demonstrate significantly higher execution complexity compared to standard transactional queries. |
| 844 | | |
| 845 | | The main performance-intensive operations identified throughout the analysis are: |
| 846 | | |
| 847 | | * multiple JOIN operations |
| 848 | | * LEFT JOIN preservation of incomplete relations |
| 849 | | * GROUP BY aggregation |
| 850 | | * COUNT(DISTINCT ...) calculations |
| 851 | | * conditional aggregation using CASE |
| 852 | | * temporal calculations using EXTRACT(EPOCH) |
| 853 | | * percentage calculations using ROUND() and NULLIF() |
| 854 | | |
| 855 | | == 1.6.1 Main Bottlenecks == |
| 856 | | |
| 857 | | || Bottleneck || Impact || |
| 858 | | || Multiple LEFT JOIN chains || Increased intermediate result size || |
| 859 | | || COUNT(DISTINCT ...) || Expensive aggregation and sorting || |
| 860 | | || GROUP BY over joined relations || Higher memory and CPU usage || |
| 861 | | || Temporal calculations || Additional CPU processing || |
| 862 | | || Missing indexes on foreign keys || Sequential scans and slow joins || |
| 863 | | |
| 864 | | The most expensive queries are: |
| 865 | | * RSVP Conversion Analysis |
| 866 | | * Venue Capacity Utilization Analysis |
| 867 | | |
| 868 | | because they process: |
| 869 | | * attendance records |
| 870 | | * RSVP records |
| 871 | | * DISTINCT aggregations |
| 872 | | * multiple optional relations |
| 873 | | |
| 874 | | == 1.6.2 Most Important Indexes == |
| 875 | | |
| 876 | | The following indexes provide the greatest performance improvements for the Phase 6 analytical workload: |
| 877 | | |
| 878 | | {{{ |
| 879 | | #!sql |
| 880 | | CREATE INDEX idx_guest_wedding |
| 881 | | ON guest(wedding_id); |
| 882 | | |
| 883 | | CREATE INDEX idx_event_wedding |
| 884 | | ON event(wedding_id); |
| 885 | | |
| 886 | | CREATE INDEX idx_event_rsvp_guest |
| 887 | | ON event_rsvp(guest_id); |
| 888 | | |
| 889 | | CREATE INDEX idx_attendance_event |
| 890 | | ON attendance(event_id); |
| 891 | | |
| 892 | | CREATE INDEX idx_venue_booking_wedding |
| 893 | | ON venue_booking(wedding_id); |
| 894 | | |
| 895 | | CREATE INDEX idx_photographer_booking_wedding |
| 896 | | ON photographer_booking(wedding_id); |
| 897 | | |
| 898 | | CREATE INDEX idx_band_booking_wedding |
| 899 | | ON band_booking(wedding_id); |
| 900 | | }}} |
| 901 | | |
| 902 | | These indexes improve: |
| 903 | | * JOIN performance |
| 904 | | * aggregation preparation |
| 905 | | * filtering efficiency |
| 906 | | * report scalability |
| 907 | | |
| 908 | | == 1.6.3 Expected Optimization Improvements == |
| 909 | | |
| 910 | | After applying the recommended indexes, PostgreSQL is expected to: |
| 911 | | * replace Sequential Scans with Index Scans |
| 912 | | * reduce JOIN execution cost |
| 913 | | * reduce aggregation preparation time |
| 914 | | * reduce overall execution latency |
| 915 | | |
| 916 | | Expected improvements include: |
| 917 | | * faster analytical report generation |
| 918 | | * lower memory consumption |
| 919 | | * lower disk I/O |
| 920 | | * better scalability for larger wedding datasets |
| 921 | | |
| 922 | | == 1.6.4 Scalability Considerations == |
| 923 | | |
| 924 | | As the database grows, the analytical queries from Phase 6 become increasingly dependent on: |
| 925 | | * index quality |
| 926 | | * efficient JOIN ordering |
| 927 | | * aggregation optimization |
| 928 | | * table statistics maintenance |
| 929 | | |
| 930 | | The largest future scalability risks are: |
| 931 | | * very large attendance datasets |
| 932 | | * large RSVP histories |
| 933 | | * repeated analytical aggregation over historical weddings |
| 934 | | |
| 935 | | To maintain acceptable performance in large-scale deployments, the following strategies are recommended: |
| 936 | | * materialized views for analytical reports |
| 937 | | * periodic archiving of historical weddings |
| 938 | | * automatic VACUUM and ANALYZE maintenance |
| 939 | | * partitioning of large attendance and RSVP tables |
| 940 | | |
| 941 | | == 1.6.5 Final Interpretation == |
| 942 | | |
| 943 | | The analysis confirms that the Phase 6 analytical reports are computationally more expensive than standard transactional operations because they combine multiple relations and perform aggregation-intensive calculations. |
| 944 | | |
| 945 | | However, with proper indexing and optimization strategies, PostgreSQL can efficiently execute these analytical reports while maintaining acceptable scalability and execution performance. |
| 946 | | |
| 947 | | == 2. Indexing Strategy & Optimization Framework == |
| 948 | | |
| 949 | | A well-designed indexing strategy is essential for maintaining predictable performance |
| 950 | | under high data volumes and concurrent user load. |
| 951 | | |
| 952 | | === 2.1 Current Index Landscape === |
| 953 | | |
| 954 | | The Phase 6 analytical reports rely heavily on foreign-key relationships and aggregation over attendance, RSVP, and booking data. |
| 955 | | |
| 956 | | A schema analysis identified the following important optimization requirements: |
| 957 | | * indexing of foreign-key columns |
| 958 | | * indexing of frequently grouped relations |
| 959 | | * optimization of JOIN paths |
| 960 | | * optimization of attendance and RSVP aggregation |
| 961 | | |
| 962 | | The most performance-sensitive tables are: |
| 963 | | * attendance |
| 964 | | * event_rsvp |
| 965 | | * venue_booking |
| 966 | | * photographer_booking |
| 967 | | * band_booking |
| 968 | | * guest |
| 969 | | * event |
| 970 | | |
| 971 | | === 2.2 Index Selectivity & Cardinality Analysis === |
| 972 | | |
| 973 | | || Column || Cardinality || Index Recommendation || |
| 974 | | || wedding_id || High || YES || |
| 975 | | || event_id || High || YES || |
| 976 | | || guest_id || High || YES || |
| 977 | | || venue_id || High || YES || |
| 978 | | || photographer_id || High || YES || |
| 979 | | || band_id || High || YES || |
| 980 | | || status || Medium || YES (combined indexes) || |
| 981 | | || event_type || Medium || Optional || |
| 982 | | || date || High || YES || |
| 983 | | |
| 984 | | Columns with high cardinality provide the best index selectivity and improve JOIN performance significantly. |
| 985 | | |
| 986 | | === 2.3 Recommended Indexes for Phase 6 Reports === |
| 987 | | |
| 988 | | ==== 2.3.1 Foreign-Key Optimization Indexes ==== |
| 989 | | |
| 990 | | {{{ |
| 991 | | #!sql |
| 992 | | CREATE INDEX idx_guest_wedding |
| 993 | | ON guest(wedding_id); |
| 994 | | |
| 995 | | CREATE INDEX idx_event_wedding |
| 996 | | ON event(wedding_id); |
| 997 | | |
| 998 | | CREATE INDEX idx_venue_booking_wedding |
| 999 | | ON venue_booking(wedding_id); |
| 1000 | | |
| 1001 | | CREATE INDEX idx_venue_booking_venue |
| 1002 | | ON venue_booking(venue_id); |
| 1003 | | |
| 1004 | | CREATE INDEX idx_photographer_booking_wedding |
| 1005 | | ON photographer_booking(wedding_id); |
| 1006 | | |
| 1007 | | CREATE INDEX idx_photographer_booking_photographer |
| 1008 | | ON photographer_booking(photographer_id); |
| 1009 | | |
| 1010 | | CREATE INDEX idx_band_booking_wedding |
| 1011 | | ON band_booking(wedding_id); |
| 1012 | | |
| 1013 | | CREATE INDEX idx_band_booking_band |
| 1014 | | ON band_booking(band_id); |
| 1015 | | |
| 1016 | | CREATE INDEX idx_attendance_event |
| 1017 | | ON attendance(event_id); |
| 1018 | | |
| 1019 | | CREATE INDEX idx_attendance_guest |
| 1020 | | ON attendance(guest_id); |
| 1021 | | |
| 1022 | | CREATE INDEX idx_event_rsvp_guest |
| 1023 | | ON event_rsvp(guest_id); |
| 1024 | | |
| 1025 | | CREATE INDEX idx_event_rsvp_event |
| 1026 | | ON event_rsvp(event_id); |
| 1027 | | }}} |
| 1028 | | |
| 1029 | | ==== Explanation ==== |
| 1030 | | |
| 1031 | | These indexes optimize: |
| 1032 | | * JOIN operations |
| 1033 | | * attendance aggregation |
| 1034 | | * RSVP lookup |
| 1035 | | * booking analysis |
| 1036 | | * analytical report generation |
| 1037 | | |
| 1038 | | The indexes reduce: |
| 1039 | | * sequential scans |
| 1040 | | * join latency |
| 1041 | | * aggregation preparation cost |
| 1042 | | |
| 1043 | | === 2.3.2 Composite Analytical Indexes ==== |
| 1044 | | |
| 1045 | | {{{ |
| 1046 | | #!sql |
| 1047 | | CREATE INDEX idx_attendance_event_status |
| 1048 | | ON attendance(event_id, status); |
| 1049 | | |
| 1050 | | CREATE INDEX idx_event_rsvp_guest_status |
| 1051 | | ON event_rsvp(guest_id, status); |
| 1052 | | |
| 1053 | | CREATE INDEX idx_event_wedding_date |
| 1054 | | ON event(wedding_id, date); |
| 1055 | | |
| 1056 | | CREATE INDEX idx_venue_booking_date |
| 1057 | | ON venue_booking(wedding_id, date); |
| 1058 | | }}} |
| 1059 | | |
| 1060 | | ==== Explanation ==== |
| 1061 | | |
| 1062 | | Composite indexes improve analytical filtering because the Phase 6 reports frequently: |
| 1063 | | * filter by status |
| 1064 | | * group by wedding/event |
| 1065 | | * analyze attendance by event |
| 1066 | | * analyze RSVP responses by status |
| 1067 | | |
| 1068 | | These indexes significantly improve: |
| 1069 | | * conditional aggregation |
| 1070 | | * GROUP BY preparation |
| 1071 | | * attendance filtering |
| 1072 | | * RSVP reporting |
| 1073 | | |
| 1074 | | === 2.4 EXPLAIN ANALYZE Validation === |
| 1075 | | |
| 1076 | | The following queries can be used to validate index utilization: |
| 1077 | | |
| 1078 | | {{{ |
| 1079 | | #!sql |
| 1080 | | EXPLAIN ANALYZE |
| 1081 | | SELECT * |
| 1082 | | FROM attendance |
| 1083 | | WHERE event_id = 1; |
| 1084 | | |
| 1085 | | EXPLAIN ANALYZE |
| 1086 | | SELECT * |
| 1087 | | FROM event_rsvp |
| 1088 | | WHERE guest_id = 1; |
| 1089 | | |
| 1090 | | EXPLAIN ANALYZE |
| 1091 | | SELECT * |
| 1092 | | FROM venue_booking |
| 1093 | | WHERE wedding_id = 1; |
| 1094 | | }}} |
| 1095 | | |
| 1096 | | Observed PostgreSQL execution plan: |
| 1097 | | |
| 1098 | | {{{ |
| 1099 | | Index Scan using idx_attendance_event on attendance |
| 1100 | | |
| 1101 | | Index Scan using idx_event_rsvp_guest on event_rsvp |
| 1102 | | |
| 1103 | | Index Scan using idx_venue_booking_wedding on venue_booking |
| 1104 | | }}} |
| 1105 | | |
| 1106 | | === 2.5 Optimization Benefits === |
| 1107 | | |
| 1108 | | The proposed indexing strategy improves: |
| 1109 | | * JOIN performance |
| 1110 | | * aggregation efficiency |
| 1111 | | * analytical reporting speed |
| 1112 | | * scalability of Phase 6 queries |
| 1113 | | |
| 1114 | | The indexing strategy reduces: |
| 1115 | | * full table scans |
| 1116 | | * unnecessary disk I/O |
| 1117 | | * aggregation overhead |
| 1118 | | * execution latency |
| 1119 | | |
| 1120 | | === 2.6 Maintenance Recommendations === |
| 1121 | | |
| 1122 | | To maintain stable analytical performance, the following maintenance procedures are recommended: |
| 1123 | | * periodic VACUUM execution |
| 1124 | | * regular ANALYZE statistics updates |
| 1125 | | * monitoring unused indexes |
| 1126 | | * reindexing fragmented indexes |
| 1127 | | * monitoring query execution plans with EXPLAIN ANALYZE |
| 1128 | | |
| 1129 | | These maintenance operations help PostgreSQL preserve optimal execution plans for the analytical reports implemented in Phase 6. |
| 1130 | | |
| 1131 | | == 3. Caching, Partitioning & Storage Optimization == |
| 1132 | | |
| 1133 | | === 3.1 Caching & Analytical Optimization === |
| 1134 | | |
| 1135 | | The Phase 6 analytical reports execute complex aggregation queries across attendance, RSVP, booking, and event relations. |
| 1136 | | |
| 1137 | | As the database grows, repeatedly calculating these analytical metrics may increase execution time and server load. |
| 1138 | | |
| 1139 | | To improve scalability, the following optimization strategies are recommended: |
| 1140 | | * materialized analytical reports |
| 1141 | | * cached aggregation results |
| 1142 | | * periodic statistics refresh |
| 1143 | | * optimization of historical analytical workloads |
| 1144 | | |
| 1145 | | === 3.2 Materialized Views === |
| 1146 | | |
| 1147 | | Materialized views can precompute expensive analytical calculations and significantly reduce report execution time. |
| 1148 | | |
| 1149 | | The following analytical reports are good candidates for materialization: |
| 1150 | | * Budget Analysis |
| 1151 | | * Venue Capacity Utilization |
| 1152 | | * RSVP Conversion Analysis |
| 1153 | | |
| 1154 | | ==== Example: RSVP Conversion Materialized View ==== |
| 1155 | | |
| 1156 | | {{{ |
| 1157 | | #!sql |
| 1158 | | CREATE MATERIALIZED VIEW mv_rsvp_conversion AS |
| 1159 | | |
| 1160 | | SELECT |
| 1161 | | w.wedding_id, |
| 1162 | | |
| 1163 | | e.event_id, |
| 1164 | | |
| 1165 | | COUNT(DISTINCT g.guest_id) |
| 1166 | | AS total_invitations, |
| 1167 | | |
| 1168 | | COUNT(DISTINCT r.response_id) |
| 1169 | | AS rsvp_responses, |
| 1170 | | |
| 1171 | | COUNT(DISTINCT CASE |
| 1172 | | WHEN r.status = 'CONFIRMED' |
| 1173 | | THEN r.response_id |
| 1174 | | END) AS confirmed_rsvps, |
| 1175 | | |
| 1176 | | COUNT(DISTINCT CASE |
| 1177 | | WHEN a.status = 'ATTENDED' |
| 1178 | | THEN a.attendance_id |
| 1179 | | END) AS actual_attendees |
| 1180 | | |
| 1181 | | FROM wedding w |
| 1182 | | |
| 1183 | | INNER JOIN event e |
| 1184 | | ON w.wedding_id = e.wedding_id |
| 1185 | | |
| 1186 | | LEFT JOIN guest g |
| 1187 | | ON w.wedding_id = g.wedding_id |
| 1188 | | |
| 1189 | | LEFT JOIN event_rsvp r |
| 1190 | | ON g.guest_id = r.guest_id |
| 1191 | | AND e.event_id = r.event_id |
| 1192 | | |
| 1193 | | LEFT JOIN attendance a |
| 1194 | | ON g.guest_id = a.guest_id |
| 1195 | | AND e.event_id = a.event_id |
| 1196 | | |
| 1197 | | GROUP BY |
| 1198 | | w.wedding_id, |
| 1199 | | e.event_id; |
| 1200 | | }}} |
| 1201 | | |
| 1202 | | ==== Benefits ==== |
| 1203 | | |
| 1204 | | Materialized views improve: |
| 1205 | | * analytical query speed |
| 1206 | | * dashboard loading |
| 1207 | | * repeated reporting performance |
| 1208 | | * scalability for historical analytics |
| 1209 | | |
| 1210 | | The materialized view stores precomputed aggregation results and avoids recalculating complex JOIN operations during every report execution. |
| 1211 | | |
| 1212 | | ==== Refreshing the Materialized View ==== |
| 1213 | | |
| 1214 | | {{{ |
| 1215 | | #!sql |
| 1216 | | REFRESH MATERIALIZED VIEW mv_rsvp_conversion; |
| 1217 | | }}} |
| 1218 | | |
| 1219 | | === 3.3 Partitioning Considerations === |
| 1220 | | |
| 1221 | | The largest future analytical tables are expected to be: |
| 1222 | | * attendance |
| 1223 | | * event_rsvp |
| 1224 | | * guest |
| 1225 | | |
| 1226 | | As historical wedding data grows, partitioning may improve scalability. |
| 1227 | | |
| 1228 | | Recommended partitioning strategy: |
| 1229 | | * partition attendance by event date |
| 1230 | | * partition RSVP records by wedding date |
| 1231 | | * archive historical weddings separately |
| 1232 | | |
| 1233 | | ==== Example: Attendance Partitioning ==== |
| 1234 | | |
| 1235 | | The following example demonstrates a conceptual partitioned version of the attendance table for large-scale deployments. |
| 1236 | | |
| 1237 | | {{{ |
| 1238 | | #!sql |
| 1239 | | CREATE TABLE attendance ( |
| 1240 | | |
| 1241 | | attendance_id SERIAL, |
| 1242 | | status VARCHAR(30), |
| 1243 | | table_number INTEGER, |
| 1244 | | role VARCHAR(50), |
| 1245 | | guest_id INTEGER, |
| 1246 | | event_id INTEGER, |
| 1247 | | attendance_date DATE, |
| 1248 | | |
| 1249 | | PRIMARY KEY(attendance_id, attendance_date) |
| 1250 | | |
| 1251 | | ) PARTITION BY RANGE (attendance_date); |
| 1252 | | }}} |
| 1253 | | |
| 1254 | | ==== Benefits ==== |
| 1255 | | |
| 1256 | | Partitioning improves: |
| 1257 | | * analytical query performance |
| 1258 | | * historical data management |
| 1259 | | * maintenance operations |
| 1260 | | * VACUUM efficiency |
| 1261 | | * scalability of large attendance datasets |
| 1262 | | |
| 1263 | | === 3.4 Storage Optimization === |
| 1264 | | |
| 1265 | | To improve long-term database performance, the following storage optimizations are recommended: |
| 1266 | | * periodic VACUUM execution |
| 1267 | | * ANALYZE statistics updates |
| 1268 | | * archival of historical wedding records |
| 1269 | | * separation of analytical and transactional workloads |
| 1270 | | |
| 1271 | | These optimizations reduce: |
| 1272 | | * table fragmentation |
| 1273 | | * outdated planner statistics |
| 1274 | | * unnecessary sequential scans |
| 1275 | | * analytical execution overhead |
| 1276 | | |
| 1277 | | === 3.5 Scalability Interpretation === |
| 1278 | | |
| 1279 | | The analytical queries from Phase 6 are aggregation-heavy and become increasingly expensive as attendance and RSVP data grows. |
| 1280 | | |
| 1281 | | Caching, materialized views, and partitioning help PostgreSQL maintain predictable execution performance even when processing large analytical datasets. |
| 1282 | | |
| 1283 | | These strategies are especially important for: |
| 1284 | | * large weddings |
| 1285 | | * long-term historical reporting |
| 1286 | | * repeated dashboard analytics |
| 1287 | | * concurrent report execution |
| 1288 | | |
| 1289 | | == 4. Concurrency, Transaction Management & Locking Strategy == |
| 1290 | | |
| 1291 | | === 4.1 Transaction Isolation Levels === |
| 1292 | | |
| 1293 | | The Wedding Planner Management System includes several operations that require transactional consistency and protection from concurrent modification. |
| 1294 | | |
| 1295 | | Examples include: |
| 1296 | | * venue booking |
| 1297 | | * attendance updates |
| 1298 | | * RSVP processing |
| 1299 | | * wedding scheduling |
| 1300 | | * event creation |
| 1301 | | |
| 1302 | | The following PostgreSQL isolation levels are recommended: |
| 1303 | | |
| 1304 | | || Isolation Level || Recommended Usage || |
| 1305 | | || READ COMMITTED || Standard CRUD operations || |
| 1306 | | || REPEATABLE READ || Venue booking validation || |
| 1307 | | || SERIALIZABLE || Critical scheduling operations || |
| 1308 | | |
| 1309 | | === 4.2 Concurrency Risks === |
| 1310 | | |
| 1311 | | The most important concurrency risks identified are: |
| 1312 | | * double-booking of venues |
| 1313 | | * simultaneous RSVP modifications |
| 1314 | | * concurrent attendance updates |
| 1315 | | * overlapping event scheduling |
| 1316 | | |
| 1317 | | Without proper transaction management, multiple users may modify the same wedding-related records simultaneously. |
| 1318 | | |
| 1319 | | === 4.3 Deadlock Prevention Strategies === |
| 1320 | | |
| 1321 | | The following practices reduce deadlock risk: |
| 1322 | | * consistent transaction ordering |
| 1323 | | * short transaction duration |
| 1324 | | * indexing foreign-key columns |
| 1325 | | * avoiding unnecessary table locking |
| 1326 | | |
| 1327 | | The most sensitive operations are: |
| 1328 | | * venue reservation |
| 1329 | | * event scheduling |
| 1330 | | * attendance confirmation |
| 1331 | | |
| 1332 | | === 4.4 Safe Venue Reservation Transaction === |
| 1333 | | |
| 1334 | | The following transaction prevents double-booking of venues. |
| 1335 | | |
| 1336 | | {{{ |
| 1337 | | #!sql |
| 1338 | | BEGIN; |
| 1339 | | |
| 1340 | | SELECT venue_id |
| 1341 | | FROM venue |
| 1342 | | WHERE venue_id = 1 |
| 1343 | | FOR UPDATE; |
| 1344 | | |
| 1345 | | INSERT INTO venue_booking ( |
| 1346 | | date, |
| 1347 | | start_time, |
| 1348 | | end_time, |
| 1349 | | status, |
| 1350 | | price, |
| 1351 | | venue_id, |
| 1352 | | wedding_id |
| 1353 | | ) |
| 1354 | | SELECT |
| 1355 | | '2025-08-20', |
| 1356 | | '18:00:00', |
| 1357 | | '23:00:00', |
| 1358 | | 'CONFIRMED', |
| 1359 | | 5000.00, |
| 1360 | | 1, |
| 1361 | | 2 |
| 1362 | | |
| 1363 | | WHERE NOT EXISTS ( |
| 1364 | | |
| 1365 | | SELECT 1 |
| 1366 | | |
| 1367 | | FROM venue_booking |
| 1368 | | |
| 1369 | | WHERE venue_id = 1 |
| 1370 | | AND date = '2025-08-20' |
| 1371 | | AND status != 'CANCELLED' |
| | 233 | ON project.attendance(status); |
| | 234 | }}} |
| | 235 | |
| | 236 | '''Note:''' Indexes on `event_rsvp(guest_id, event_id)`, `attendance(guest_id, event_id)`, `guest(wedding_id)`, and `event(wedding_id)` already exist from earlier phases. |
| | 237 | |
| | 238 | ==== EXPLAIN ANALYZE – Before Indexes ==== |
| | 239 | |
| | 240 | {{{ |
| | 241 | GroupAggregate (cost=108.70..137.20 rows=300 width=562) |
| | 242 | (actual time=0.860..1.039 rows=13 loops=1) |
| | 243 | Group Key: w.wedding_id, e.event_id, u.first_name, u.last_name |
| | 244 | -> Sort (cost=108.70..109.45 rows=300 width=484) |
| | 245 | (actual time=0.812..0.828 rows=202 loops=1) |
| | 246 | Sort Method: quicksort Memory: 41kB |
| | 247 | -> Hash Left Join (cost=77.15..96.35 rows=300 width=484) |
| | 248 | (actual time=0.241..0.375 rows=202 loops=1) |
| | 249 | Hash Cond: ((g.guest_id = a.guest_id) AND (e.event_id = a.event_id)) |
| | 250 | -> Hash Left Join (cost=57.90..75.53 rows=300 width=402) |
| | 251 | (actual time=0.198..0.291 rows=202 loops=1) |
| | 252 | Hash Cond: ((g.guest_id = r.guest_id) AND (e.event_id = r.event_id)) |
| | 253 | -> Hash Left Join |
| | 254 | Hash Cond: (w.wedding_id = g.wedding_id) |
| | 255 | -> Hash Join |
| | 256 | -> Seq Scan on event e |
| | 257 | -> Seq Scan on wedding w |
| | 258 | -> Seq Scan on guest g |
| | 259 | -> Seq Scan on event_rsvp r (rows=104) |
| | 260 | -> Seq Scan on attendance a (rows=370) |
| | 261 | ** |
| | 262 | Planning Time: 1.926 ms |
| | 263 | Execution Time: 1.254 ms** |
| | 264 | }}} |
| | 265 | |
| | 266 | ==== EXPLAIN ANALYZE – After Indexes ==== |
| | 267 | |
| | 268 | {{{ |
| | 269 | GroupAggregate (cost=91.82..120.32 rows=300 width=562) |
| | 270 | (actual time=0.813..0.992 rows=13 loops=1) |
| | 271 | Group Key: w.wedding_id, e.event_id, u.first_name, u.last_name |
| | 272 | -> Sort (cost=91.82..92.57 rows=300 width=484) |
| | 273 | (actual time=0.776..0.792 rows=202 loops=1) |
| | 274 | Sort Method: quicksort Memory: 41kB |
| | 275 | -> Hash Left Join (cost=60.27..79.48 rows=300 width=484) |
| | 276 | (actual time=0.210..0.341 rows=202 loops=1) |
| | 277 | Hash Cond: ((g.guest_id = a.guest_id) AND (e.event_id = a.event_id)) |
| | 278 | -> Hash Left Join (cost=58.27..75.90 rows=300 width=402) |
| | 279 | (actual time=0.176..0.266 rows=202 loops=1) |
| | 280 | Hash Cond: ((g.guest_id = r.guest_id) AND (e.event_id = r.event_id)) |
| | 281 | -> Hash Left Join |
| | 282 | Hash Cond: (w.wedding_id = g.wedding_id) |
| | 283 | -> Hash Join |
| | 284 | -> Seq Scan on event e |
| | 285 | -> Seq Scan on wedding w |
| | 286 | -> Seq Scan on guest g |
| | 287 | -> Seq Scan on event_rsvp r (rows=119) |
| | 288 | -> Seq Scan on attendance a (rows=40) |
| | 289 | |
| | 290 | **Planning Time: 1.314 ms |
| | 291 | Execution Time: 1.122 ms** |
| | 292 | }}} |
| | 293 | |
| | 294 | ==== Performance Comparison ==== |
| | 295 | |
| | 296 | ||= Metric =||= Without Indexes =||= With Indexes =||= Improvement =|| |
| | 297 | || Planning Time || 1.926 ms || 1.314 ms || -31.8% || |
| | 298 | || Execution Time || 1.254 ms || 1.122 ms || -10.5% || |
| | 299 | || Total Query Cost || 108.70..137.20 || 91.82..120.32 || -15.6% || |
| | 300 | || attendance rows estimated || 370 || 40 || -89.2% || |
| | 301 | || Scan Type || Seq Scan (all tables) || Seq Scan (all tables) || — || |
| | 302 | |
| | 303 | ==== Interpretation ==== |
| | 304 | |
| | 305 | The most significant improvement in this scenario is the attendance table row estimation: PostgreSQL now correctly estimates '''40 rows''' instead of 370, reflecting accurate statistics collected during index creation. This results in a '''15.6% reduction in total estimated query cost''' (137.20 → 120.32). |
| | 306 | |
| | 307 | Sequential Scans are retained on all tables because the current dataset is small. The query planner correctly determines that Hash Join with Seq Scan is cheaper than index traversal for tables of this size. |
| | 308 | |
| | 309 | The new indexes `idx_event_rsvp_status` and `idx_attendance_status` enable efficient status-based filtering for the `CASE WHEN r.status = 'accepted'` and `CASE WHEN a.status = 'attending'` conditional aggregations. As the number of RSVPs and attendance records grows across multiple weddings, these indexes will eliminate full table scans and significantly reduce execution time. |
| | 310 | |
| | 311 | ---- |
| | 312 | |
| | 313 | == Security Measures == |
| | 314 | |
| | 315 | === SQL Injection Prevention === |
| | 316 | |
| | 317 | The Wedding Planner backend (Node.js with Express and the `pg` PostgreSQL driver) uses '''parameterized queries''' exclusively throughout all database operations. User input is never concatenated directly into SQL strings. |
| | 318 | |
| | 319 | All query parameters are passed as separate values using the `$1`, `$2`, `$n` placeholder syntax provided by the `pg` library. The PostgreSQL driver binds these values as typed parameters, ensuring they are never interpreted as executable SQL code. |
| | 320 | |
| | 321 | {{{ |
| | 322 | #!javascript |
| | 323 | // Example: parameterized query for guest lookup |
| | 324 | const result = await pool.query( |
| | 325 | `SELECT * FROM project.guest |
| | 326 | WHERE wedding_id = $1 |
| | 327 | AND guest_id = $2`, |
| | 328 | [weddingId, guestId] |
| 1373 | | |
| 1374 | | COMMIT; |
| 1375 | | }}} |
| 1376 | | |
| 1377 | | === Explanation === |
| 1378 | | |
| 1379 | | `FOR UPDATE` locks the selected venue row during the transaction. |
| 1380 | | |
| 1381 | | This prevents concurrent transactions from simultaneously booking the same venue for overlapping dates. |
| 1382 | | |
| 1383 | | The `NOT EXISTS` condition ensures that no conflicting booking already exists. |
| 1384 | | |
| 1385 | | === Validation === |
| 1386 | | |
| 1387 | | {{{ |
| 1388 | | #!sql |
| 1389 | | SELECT |
| 1390 | | venue_id, |
| 1391 | | date, |
| 1392 | | COUNT(*) |
| 1393 | | |
| 1394 | | FROM venue_booking |
| 1395 | | |
| 1396 | | WHERE status != 'CANCELLED' |
| 1397 | | |
| 1398 | | GROUP BY |
| 1399 | | venue_id, |
| 1400 | | date |
| 1401 | | |
| 1402 | | HAVING COUNT(*) > 1; |
| 1403 | | }}} |
| 1404 | | |
| 1405 | | Expected result: |
| 1406 | | * empty result set |
| 1407 | | |
| 1408 | | Any returned rows indicate conflicting venue bookings. |
| 1409 | | |
| 1410 | | === 4.5 Row-Level Locking === |
| 1411 | | |
| 1412 | | The system primarily relies on: |
| 1413 | | * row-level locks |
| 1414 | | * transaction isolation |
| 1415 | | * foreign-key consistency |
| 1416 | | |
| 1417 | | Row-level locking is preferred because it: |
| 1418 | | * minimizes blocking |
| 1419 | | * improves concurrency |
| 1420 | | * prevents unnecessary table-wide locks |
| 1421 | | |
| 1422 | | === 4.6 Transaction Scalability === |
| 1423 | | |
| 1424 | | As the number of weddings and concurrent users increases, transaction management becomes increasingly important. |
| 1425 | | |
| 1426 | | The following practices improve scalability: |
| 1427 | | * keeping transactions short |
| 1428 | | * indexing transactional lookup columns |
| 1429 | | * separating analytical reports from transactional operations |
| 1430 | | * avoiding long-running locks |
| 1431 | | |
| 1432 | | === 4.7 Final Interpretation === |
| 1433 | | |
| 1434 | | The Wedding Planner Management System contains several scheduling and booking operations that require transactional consistency. |
| 1435 | | |
| 1436 | | PostgreSQL transaction isolation and row-level locking mechanisms help prevent: |
| 1437 | | * double-booking |
| 1438 | | * inconsistent RSVP updates |
| 1439 | | * concurrent attendance conflicts |
| 1440 | | * invalid scheduling states |
| 1441 | | |
| 1442 | | Proper transaction management ensures both: |
| 1443 | | * data integrity |
| 1444 | | * stable concurrent system behavior |
| 1445 | | |
| 1446 | | == 5. Performance Analysis with EXPLAIN / EXPLAIN ANALYZE == |
| 1447 | | |
| 1448 | | === 5.1 Purpose of EXPLAIN and EXPLAIN ANALYZE === |
| 1449 | | |
| 1450 | | PostgreSQL provides several commands for analyzing query execution behavior. |
| 1451 | | |
| 1452 | | || Command || Purpose || |
| 1453 | | || EXPLAIN || Shows the planned execution strategy without running the query || |
| 1454 | | || EXPLAIN ANALYZE || Executes the query and shows real execution statistics || |
| 1455 | | || EXPLAIN (ANALYZE, BUFFERS) || Shows execution statistics plus buffer/cache usage || |
| 1456 | | |
| 1457 | | In this phase, `EXPLAIN ANALYZE` is used to evaluate the complex analytical queries from Phase 6. |
| 1458 | | |
| 1459 | | It helps identify: |
| 1460 | | * scan types |
| 1461 | | * join strategies |
| 1462 | | * aggregation methods |
| 1463 | | * actual execution time |
| 1464 | | * row counts |
| 1465 | | * loops |
| 1466 | | * possible bottlenecks |
| 1467 | | |
| 1468 | | === 5.2 Important Execution Plan Elements === |
| 1469 | | |
| 1470 | | || Plan Element || Meaning || |
| 1471 | | || Seq Scan || PostgreSQL scans the entire table || |
| 1472 | | || Index Scan || PostgreSQL uses an index to locate rows faster || |
| 1473 | | || Hash Join || PostgreSQL builds a hash table to join larger datasets || |
| 1474 | | || Nested Loop || PostgreSQL repeatedly scans one relation for each row of another relation || |
| 1475 | | || !HashAggregate || PostgreSQL performs grouping and aggregation using a hash table || |
| 1476 | | || Sort || PostgreSQL sorts rows for ORDER BY or aggregation operations || |
| 1477 | | |
| 1478 | | For Phase 6 reports, the most common expected elements are: |
| 1479 | | * Hash Join |
| 1480 | | * !HashAggregate |
| 1481 | | * Index Scan |
| 1482 | | * Seq Scan on small tables |
| 1483 | | * Sort |
| 1484 | | |
| 1485 | | === 5.3 Sequential Scan vs Index Scan === |
| 1486 | | |
| 1487 | | A Sequential Scan is not always a problem. |
| 1488 | | |
| 1489 | | PostgreSQL may choose a Sequential Scan when: |
| 1490 | | * the table is small |
| 1491 | | * most rows are needed |
| 1492 | | * the cost of using an index is higher than scanning the table |
| 1493 | | |
| 1494 | | However, for large Phase 6 analytical tables such as: |
| 1495 | | * attendance |
| 1496 | | * event_rsvp |
| 1497 | | * guest |
| 1498 | | * booking tables |
| 1499 | | |
| 1500 | | Index Scans are preferred when filtering or joining by foreign-key columns. |
| 1501 | | |
| 1502 | | === 5.4 Example: Attendance Lookup Before Optimization === |
| 1503 | | |
| 1504 | | {{{ |
| 1505 | | #!sql |
| 1506 | | EXPLAIN ANALYZE |
| 1507 | | SELECT * |
| 1508 | | FROM attendance |
| 1509 | | WHERE event_id = 1; |
| 1510 | | }}} |
| 1511 | | |
| 1512 | | Without an index on `attendance(event_id)`, PostgreSQL may use: |
| 1513 | | |
| 1514 | | {{{ |
| 1515 | | Seq Scan on attendance |
| 1516 | | Filter: (event_id = 1) |
| 1517 | | }}} |
| 1518 | | |
| 1519 | | This means that all attendance rows are scanned before matching rows are returned. |
| 1520 | | |
| 1521 | | For large attendance tables, this increases: |
| 1522 | | * disk I/O |
| 1523 | | * CPU usage |
| 1524 | | * query latency |
| 1525 | | |
| 1526 | | === 5.5 Example: Attendance Lookup After Optimization === |
| 1527 | | |
| 1528 | | {{{ |
| 1529 | | #!sql |
| 1530 | | CREATE INDEX idx_attendance_event |
| 1531 | | ON attendance(event_id); |
| 1532 | | |
| 1533 | | EXPLAIN ANALYZE |
| 1534 | | SELECT * |
| 1535 | | FROM attendance |
| 1536 | | WHERE event_id = 1; |
| 1537 | | }}} |
| 1538 | | |
| 1539 | | Observed execution plan: |
| 1540 | | |
| 1541 | | {{{ |
| 1542 | | Index Scan using idx_attendance_event on attendance |
| 1543 | | Index Cond: (event_id = 1) |
| 1544 | | }}} |
| 1545 | | |
| 1546 | | This confirms that PostgreSQL can directly locate attendance records for a specific event. |
| 1547 | | |
| 1548 | | === 5.6 EXPLAIN ANALYZE for Phase 6 Reports === |
| 1549 | | |
| 1550 | | The Phase 6 reports should be analyzed using EXPLAIN ANALYZE because they include: |
| 1551 | | * joins across multiple tables |
| 1552 | | * aggregation |
| 1553 | | * distinct counting |
| 1554 | | * conditional calculations |
| 1555 | | |
| 1556 | | The analysis should be performed on: |
| 1557 | | * Budget Analysis query |
| 1558 | | * Venue Capacity query |
| 1559 | | * RSVP Conversion query |
| 1560 | | |
| 1561 | | These queries represent the real analytical workload of the Wedding Planner Management System. |
| 1562 | | |
| 1563 | | === 5.7 Interpreting Execution Time === |
| 1564 | | |
| 1565 | | When reading EXPLAIN ANALYZE output, the most important values are: |
| 1566 | | * Planning Time |
| 1567 | | * Execution Time |
| 1568 | | * actual rows |
| 1569 | | * loops |
| 1570 | | * scan type |
| 1571 | | * join type |
| 1572 | | |
| 1573 | | High execution time may indicate: |
| 1574 | | * missing indexes |
| 1575 | | * inefficient JOIN order |
| 1576 | | * expensive aggregation |
| 1577 | | * large intermediate result sets |
| 1578 | | * outdated PostgreSQL statistics |
| 1579 | | |
| 1580 | | === 5.8 Validation of Index Usage === |
| 1581 | | |
| 1582 | | After creating indexes, index usage can be checked using: |
| 1583 | | |
| 1584 | | {{{ |
| 1585 | | #!sql |
| 1586 | | SELECT |
| 1587 | | indexrelname AS index_name, |
| 1588 | | idx_scan AS times_used, |
| 1589 | | idx_tup_read AS tuples_read, |
| 1590 | | idx_tup_fetch AS tuples_fetched |
| 1591 | | |
| 1592 | | FROM pg_stat_user_indexes |
| 1593 | | |
| 1594 | | WHERE indexrelname IN ( |
| 1595 | | 'idx_attendance_event', |
| 1596 | | 'idx_guest_wedding', |
| 1597 | | 'idx_event_rsvp_guest', |
| 1598 | | 'idx_venue_booking_wedding' |
| | 330 | }}} |
| | 331 | |
| | 332 | {{{ |
| | 333 | #!javascript |
| | 334 | // Example: parameterized INSERT for new booking |
| | 335 | await pool.query( |
| | 336 | `INSERT INTO project.venue_booking |
| | 337 | (date, start_time, end_time, status, price, venue_id, wedding_id) |
| | 338 | VALUES ($1, $2, $3, $4, $5, $6, $7)`, |
| | 339 | [date, startTime, endTime, status, price, venueId, weddingId] |
| 1681 | | }}} |
| 1682 | | |
| 1683 | | === Explanation === |
| 1684 | | |
| 1685 | | This policy ensures that each organizer can only access weddings linked to their own user account. |
| 1686 | | |
| 1687 | | This prevents unauthorized access to: |
| 1688 | | * guest lists |
| 1689 | | * attendance records |
| 1690 | | * RSVP information |
| 1691 | | * booking information |
| 1692 | | |
| 1693 | | === Validation === |
| 1694 | | |
| 1695 | | {{{ |
| 1696 | | #!sql |
| 1697 | | SELECT * |
| 1698 | | FROM wedding; |
| 1699 | | }}} |
| 1700 | | |
| 1701 | | Expected behavior: |
| 1702 | | * users only see weddings associated with their own user_id |
| 1703 | | |
| 1704 | | === Security Benefits === |
| 1705 | | |
| 1706 | | The proposed RBAC and RLS configuration improves: |
| 1707 | | * access control |
| 1708 | | * data isolation |
| 1709 | | * organizer privacy |
| 1710 | | * protection against unauthorized data access |
| 1711 | | |
| 1712 | | These mechanisms are especially important in multi-user environments where several organizers use the system simultaneously. |
| 1713 | | |
| 1714 | | === 6.2 Encryption Strategy === |
| 1715 | | |
| 1716 | | ==== Encryption in Transit ==== |
| 1717 | | |
| 1718 | | All communication between the application and PostgreSQL database should use encrypted connections. |
| 1719 | | |
| 1720 | | Recommended protections: |
| 1721 | | * TLS-secured database connections |
| 1722 | | * encrypted API communication |
| 1723 | | * secure administrator access |
| 1724 | | |
| 1725 | | These protections prevent interception of: |
| 1726 | | * guest information |
| 1727 | | * RSVP records |
| 1728 | | * attendance data |
| 1729 | | * wedding schedules |
| 1730 | | |
| 1731 | | ==== Encryption at Rest ==== |
| 1732 | | |
| 1733 | | Sensitive information stored inside the database should be protected using encryption mechanisms. |
| 1734 | | |
| 1735 | | Sensitive fields include: |
| 1736 | | * phone numbers |
| 1737 | | * email addresses |
| 1738 | | * guest notes |
| 1739 | | * RSVP comments |
| 1740 | | |
| 1741 | | PostgreSQL extensions such as `pgcrypto` can be used for column-level encryption. |
| 1742 | | |
| 1743 | | ==== Example: pgcrypto Encryption ==== |
| 1744 | | |
| 1745 | | {{{ |
| 1746 | | #!sql |
| 1747 | | INSERT INTO guest ( |
| 1748 | | first_name, |
| 1749 | | last_name, |
| 1750 | | email |
| 1751 | | ) |
| 1752 | | VALUES ( |
| 1753 | | 'Ivan', |
| 1754 | | 'Petrov', |
| 1755 | | pgp_sym_encrypt( |
| 1756 | | 'ivan@email.com', |
| 1757 | | 'wedding_secret_key' |
| 1758 | | ) |
| 1759 | | ); |
| 1760 | | }}} |
| 1761 | | |
| 1762 | | ==== Example: Decryption ==== |
| 1763 | | |
| 1764 | | {{{ |
| 1765 | | #!sql |
| 1766 | | SELECT |
| 1767 | | first_name, |
| 1768 | | last_name, |
| 1769 | | |
| 1770 | | pgp_sym_decrypt( |
| 1771 | | email::bytea, |
| 1772 | | 'wedding_secret_key' |
| 1773 | | ) AS decrypted_email |
| 1774 | | |
| 1775 | | FROM guest; |
| 1776 | | }}} |
| 1777 | | |
| 1778 | | ==== Implementation Note ==== |
| 1779 | | |
| 1780 | | The encryption example demonstrates the conceptual usage of PostgreSQL `pgcrypto`. |
| 1781 | | |
| 1782 | | In a production implementation, encrypted values should be stored in dedicated encrypted columns (for example `email_encrypted BYTEA`) instead of replacing the original plaintext column directly. |
| 1783 | | |
| 1784 | | This approach improves schema consistency and avoids datatype conflicts during encryption and decryption operations. |
| 1785 | | |
| 1786 | | ==== Validation ==== |
| 1787 | | |
| 1788 | | {{{ |
| 1789 | | #!sql |
| 1790 | | SELECT email |
| 1791 | | FROM guest; |
| 1792 | | }}} |
| 1793 | | |
| 1794 | | Expected result: |
| 1795 | | * encrypted binary data instead of readable plaintext |
| 1796 | | |
| 1797 | | === Security Benefits === |
| 1798 | | |
| 1799 | | Encryption improves: |
| 1800 | | * protection of sensitive guest data |
| 1801 | | * privacy of wedding participants |
| 1802 | | * protection against unauthorized database access |
| 1803 | | * compliance with modern data-protection practices |
| 1804 | | |
| 1805 | | === 6.3 Data Masking & Anonymization === |
| 1806 | | |
| 1807 | | The Wedding Planner Management System stores sensitive personal information related to wedding guests and organizers. |
| 1808 | | |
| 1809 | | For analytical and testing environments, sensitive information should be masked or anonymized. |
| 1810 | | |
| 1811 | | Sensitive information includes: |
| 1812 | | * guest names |
| 1813 | | * phone numbers |
| 1814 | | * email addresses |
| 1815 | | * RSVP comments |
| 1816 | | |
| 1817 | | === Example: Masked Reporting View === |
| 1818 | | |
| 1819 | | {{{ |
| 1820 | | #!sql |
| 1821 | | CREATE VIEW v_guest_reporting AS |
| 1822 | | |
| 1823 | | SELECT |
| 1824 | | guest_id, |
| 1825 | | |
| 1826 | | CONCAT( |
| 1827 | | LEFT(first_name, 1), |
| 1828 | | REPEAT('*', LENGTH(first_name) - 1) |
| 1829 | | ) AS masked_first_name, |
| 1830 | | |
| 1831 | | CONCAT( |
| 1832 | | LEFT(last_name, 1), |
| 1833 | | REPEAT('*', LENGTH(last_name) - 1) |
| 1834 | | ) AS masked_last_name, |
| 1835 | | |
| 1836 | | CONCAT( |
| 1837 | | REPEAT('*', 5), |
| 1838 | | RIGHT(email, POSITION('@' IN email)) |
| 1839 | | ) AS masked_email, |
| 1840 | | |
| 1841 | | wedding_id |
| 1842 | | |
| 1843 | | FROM guest; |
| 1844 | | }}} |
| 1845 | | |
| 1846 | | === Explanation === |
| 1847 | | |
| 1848 | | The view masks personally identifiable information while still allowing analytical reporting. |
| 1849 | | |
| 1850 | | This approach is useful for: |
| 1851 | | * testing environments |
| 1852 | | * reporting dashboards |
| 1853 | | * analytical exports |
| 1854 | | * demonstration systems |
| 1855 | | |
| 1856 | | === Validation === |
| 1857 | | |
| 1858 | | {{{ |
| 1859 | | #!sql |
| 1860 | | SELECT * |
| 1861 | | FROM v_guest_reporting; |
| 1862 | | }}} |
| 1863 | | |
| 1864 | | Expected result: |
| 1865 | | * masked guest names |
| 1866 | | * masked email addresses |
| 1867 | | * preserved wedding relationships |
| 1868 | | |
| 1869 | | === Security Benefits === |
| 1870 | | |
| 1871 | | Data masking improves: |
| 1872 | | * privacy protection |
| 1873 | | * safer analytical reporting |
| 1874 | | * reduced exposure of sensitive data |
| 1875 | | * compliance with data-protection principles |
| 1876 | | |
| 1877 | | === 6.4 SQL Injection Prevention === |
| 1878 | | |
| 1879 | | The Wedding Planner Management System accepts user-generated input through: |
| 1880 | | * RSVP forms |
| 1881 | | * guest registration |
| 1882 | | * wedding creation |
| 1883 | | * event scheduling |
| 1884 | | * attendance management |
| 1885 | | |
| 1886 | | If SQL queries are constructed incorrectly, malicious input may compromise database security. |
| 1887 | | |
| 1888 | | === Unsafe Query Example === |
| 1889 | | |
| 1890 | | The following example is vulnerable to SQL injection: |
| 1891 | | |
| 1892 | | {{{ |
| 1893 | | #!sql |
| 1894 | | query = |
| 1895 | | "SELECT * FROM guest WHERE first_name = '" + user_input + "'" |
| 1896 | | }}} |
| 1897 | | |
| 1898 | | This approach allows attackers to manipulate SQL syntax through malicious input. |
| 1899 | | |
| 1900 | | === Secure Parameterized Query === |
| 1901 | | |
| 1902 | | The recommended approach is parameterized execution. |
| 1903 | | |
| 1904 | | {{{ |
| 1905 | | #!sql |
| 1906 | | PREPARE guest_lookup(TEXT) AS |
| 1907 | | |
| 1908 | | SELECT |
| 1909 | | guest_id, |
| 1910 | | first_name, |
| 1911 | | last_name, |
| 1912 | | wedding_id |
| 1913 | | |
| 1914 | | FROM guest |
| 1915 | | |
| 1916 | | WHERE first_name = $1; |
| 1917 | | }}} |
| 1918 | | |
| 1919 | | === Example Execution === |
| 1920 | | |
| 1921 | | {{{ |
| 1922 | | #!sql |
| 1923 | | EXECUTE guest_lookup('Ivan'); |
| 1924 | | }}} |
| 1925 | | |
| 1926 | | === Explanation === |
| 1927 | | |
| 1928 | | Parameterized queries separate: |
| 1929 | | * SQL structure |
| 1930 | | * user-provided input |
| 1931 | | |
| 1932 | | This prevents user input from being interpreted as executable SQL code. |
| 1933 | | |
| 1934 | | === Security Benefits === |
| 1935 | | |
| 1936 | | Parameterized queries protect against: |
| 1937 | | * unauthorized data access |
| 1938 | | * SQL injection attacks |
| 1939 | | * manipulation of RSVP data |
| 1940 | | * unauthorized attendance modification |
| 1941 | | * wedding data corruption |
| 1942 | | |
| 1943 | | === Recommended Practices === |
| 1944 | | |
| 1945 | | The following practices are recommended throughout the system: |
| 1946 | | * parameterized queries |
| 1947 | | * input validation |
| 1948 | | * restricted database permissions |
| 1949 | | * transaction isolation |
| 1950 | | * prepared statements |
| 1951 | | |
| 1952 | | These protections significantly improve overall database security. |
| 1953 | | |
| 1954 | | === 6.5 GDPR Compliance === |
| 1955 | | |
| 1956 | | The Wedding Planner Management System stores personal information related to: |
| 1957 | | * wedding organizers |
| 1958 | | * guests |
| 1959 | | * RSVP records |
| 1960 | | * attendance information |
| 1961 | | |
| 1962 | | Because the system processes personal data, several GDPR-related principles should be respected. |
| 1963 | | |
| 1964 | | === Data Minimization === |
| 1965 | | |
| 1966 | | Only information necessary for wedding organization should be stored. |
| 1967 | | |
| 1968 | | Examples: |
| 1969 | | * guest names |
| 1970 | | * RSVP responses |
| 1971 | | * attendance status |
| 1972 | | * organizer contact information |
| 1973 | | |
| 1974 | | Unnecessary personal information should not be collected. |
| 1975 | | |
| 1976 | | === Access Control === |
| 1977 | | |
| 1978 | | Only authorized users should access: |
| 1979 | | * guest information |
| 1980 | | * RSVP records |
| 1981 | | * attendance reports |
| 1982 | | * wedding schedules |
| 1983 | | |
| 1984 | | Role-Based Access Control and Row-Level Security help enforce this restriction. |
| 1985 | | |
| 1986 | | === Right to Access === |
| 1987 | | |
| 1988 | | Users should be able to: |
| 1989 | | * view their stored information |
| 1990 | | * verify RSVP information |
| 1991 | | * review attendance-related records |
| 1992 | | |
| 1993 | | === Right to Deletion === |
| 1994 | | |
| 1995 | | When requested, personal information should be removable from the database. |
| 1996 | | |
| 1997 | | Example deletion operation: |
| 1998 | | |
| 1999 | | {{{ |
| 2000 | | #!sql |
| 2001 | | DELETE FROM guest |
| 2002 | | WHERE guest_id = 10; |
| 2003 | | }}} |
| 2004 | | |
| 2005 | | === Backup Protection === |
| 2006 | | |
| 2007 | | Backups containing personal information should: |
| 2008 | | * remain encrypted |
| 2009 | | * be access-controlled |
| 2010 | | * be stored securely |
| 2011 | | |
| 2012 | | === GDPR Benefits === |
| 2013 | | |
| 2014 | | Applying GDPR principles improves: |
| 2015 | | * privacy protection |
| 2016 | | * organizer trust |
| 2017 | | * legal compliance |
| 2018 | | * secure handling of wedding-related data |
| 2019 | | |
| 2020 | | === 6.6 Backup, Restore & Disaster Recovery === |
| 2021 | | |
| 2022 | | The Wedding Planner Management System stores important operational and personal data. |
| 2023 | | |
| 2024 | | Database backups are necessary to protect: |
| 2025 | | * wedding schedules |
| 2026 | | * RSVP records |
| 2027 | | * attendance information |
| 2028 | | * booking data |
| 2029 | | * organizer information |
| 2030 | | |
| 2031 | | === Recommended Backup Strategy === |
| 2032 | | |
| 2033 | | || Backup Type || Frequency || |
| 2034 | | || Full Backup || Daily || |
| 2035 | | || Incremental Backup || Every few hours || |
| 2036 | | || WAL Archiving || Continuous || |
| 2037 | | |
| 2038 | | === Backup Validation === |
| 2039 | | |
| 2040 | | After backup restoration, database consistency should be verified. |
| 2041 | | |
| 2042 | | {{{ |
| 2043 | | #!sql |
| 2044 | | SELECT |
| 2045 | | 'wedding' AS table_name, |
| 2046 | | COUNT(*) AS row_count |
| 2047 | | FROM wedding |
| 2048 | | |
| 2049 | | UNION ALL |
| 2050 | | |
| 2051 | | SELECT |
| 2052 | | 'guest', |
| 2053 | | COUNT(*) |
| 2054 | | FROM guest |
| 2055 | | |
| 2056 | | UNION ALL |
| 2057 | | |
| 2058 | | SELECT |
| 2059 | | 'event', |
| 2060 | | COUNT(*) |
| 2061 | | FROM event |
| 2062 | | |
| 2063 | | UNION ALL |
| 2064 | | |
| 2065 | | SELECT |
| 2066 | | 'attendance', |
| 2067 | | COUNT(*) |
| 2068 | | FROM attendance; |
| 2069 | | }}} |
| 2070 | | |
| 2071 | | === Explanation === |
| 2072 | | |
| 2073 | | The validation query confirms that: |
| 2074 | | * important tables were restored correctly |
| 2075 | | * row counts are preserved |
| 2076 | | * critical wedding data still exists |
| 2077 | | |
| 2078 | | === Disaster Recovery Recommendations === |
| 2079 | | |
| 2080 | | The following practices improve recovery reliability: |
| 2081 | | * encrypted backups |
| 2082 | | * off-site backup storage |
| 2083 | | * periodic restore testing |
| 2084 | | * automatic backup scheduling |
| 2085 | | |
| 2086 | | === Security Benefits === |
| 2087 | | |
| 2088 | | Proper backup management improves: |
| 2089 | | * data recovery capability |
| 2090 | | * protection against accidental deletion |
| 2091 | | * recovery after hardware failure |
| 2092 | | * long-term database reliability |
| 2093 | | |
| 2094 | | === 6.7 Audit Logging === |
| 2095 | | |
| 2096 | | Audit logging is important for tracking sensitive modifications inside the Wedding Planner Management System. |
| 2097 | | |
| 2098 | | The system should record: |
| 2099 | | * RSVP modifications |
| 2100 | | * attendance updates |
| 2101 | | * venue booking changes |
| 2102 | | * wedding schedule modifications |
| 2103 | | * guest list updates |
| 2104 | | |
| 2105 | | Audit logging improves: |
| 2106 | | * accountability |
| 2107 | | * traceability |
| 2108 | | * security monitoring |
| 2109 | | * recovery after accidental modification |
| 2110 | | |
| 2111 | | === Example Audit Table === |
| 2112 | | |
| 2113 | | {{{ |
| 2114 | | #!sql |
| 2115 | | CREATE TABLE audit_log ( |
| 2116 | | |
| 2117 | | audit_id SERIAL PRIMARY KEY, |
| 2118 | | |
| 2119 | | table_name VARCHAR(100), |
| 2120 | | |
| 2121 | | operation_type VARCHAR(30), |
| 2122 | | |
| 2123 | | changed_by VARCHAR(100), |
| 2124 | | |
| 2125 | | changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| 2126 | | |
| 2127 | | affected_record_id INTEGER |
| 2128 | | ); |
| 2129 | | }}} |
| 2130 | | |
| 2131 | | === Example Trigger Function === |
| 2132 | | |
| 2133 | | {{{ |
| 2134 | | #!sql |
| 2135 | | CREATE OR REPLACE FUNCTION log_guest_changes() |
| 2136 | | |
| 2137 | | RETURNS TRIGGER AS $$ |
| 2138 | | |
| 2139 | | BEGIN |
| 2140 | | |
| 2141 | | INSERT INTO audit_log ( |
| 2142 | | table_name, |
| 2143 | | operation_type, |
| 2144 | | changed_by, |
| 2145 | | affected_record_id |
| 2146 | | ) |
| 2147 | | |
| 2148 | | VALUES ( |
| 2149 | | 'guest', |
| 2150 | | TG_OP, |
| 2151 | | CURRENT_USER, |
| 2152 | | NEW.guest_id |
| | 363 | |
| | 364 | // Verifying password on login |
| | 365 | const match = await bcrypt.compare(plainPassword, storedHash); |
| | 366 | }}} |
| | 367 | |
| | 368 | The `bcrypt` algorithm applies a salt automatically, ensuring that two identical passwords produce different hash values in the database. |
| | 369 | |
| | 370 | === Session-Based Authentication === |
| | 371 | |
| | 372 | Access to all protected API endpoints is controlled using `express-session`. A `requireAuth` middleware function verifies that a valid session exists before any database operation is executed. |
| | 373 | |
| | 374 | {{{ |
| | 375 | #!javascript |
| | 376 | function requireAuth(req, res, next) { |
| | 377 | if (!req.session || !req.session.user) { |
| | 378 | return res.status(401).json({ error: 'Not authenticated.' }); |
| | 379 | } |
| | 380 | next(); |
| | 381 | } |
| | 382 | |
| | 383 | // Applied to all protected routes |
| | 384 | app.get('/api/weddings', requireAuth, async (req, res) => { |
| | 385 | const result = await pool.query( |
| | 386 | `SELECT * FROM project.wedding |
| | 387 | WHERE user_id = $1 |
| | 388 | ORDER BY date DESC`, |
| | 389 | [req.session.user.user_id] |