| 120 | | |
| 121 | | DROP INDEX IF EXISTS uk_course_translate_course_language; |
| 122 | | DROP INDEX IF EXISTS idx_course_version_course_id; |
| 123 | | DROP INDEX IF EXISTS idx_enrollment_course_version_id; |
| 124 | | DROP INDEX IF EXISTS uk_review_enrollment; |
| 125 | | |
| 126 | | -- run 1: no indexes |
| 127 | | |
| 128 | | SELECT test_popular_courses(); |
| 129 | | |
| 130 | | -- run 2: index uk_course_translate_course_language |
| 131 | | |
| 132 | | CREATE UNIQUE INDEX uk_course_translate_course_language |
| 133 | | ON course_translate(course_id, language); |
| 134 | | ANALYZE course_translate; |
| 135 | | |
| 136 | | SELECT test_popular_courses(); |
| 137 | | |
| 138 | | -- run 3: index uk_course_translate_course_language + idx_course_version_course_id |
| 139 | | |
| 140 | | CREATE INDEX idx_course_version_course_id |
| 141 | | ON course_version(course_id); |
| 142 | | ANALYZE course_version; |
| 143 | | |
| 144 | | SELECT test_popular_courses(); |
| 145 | | |
| 146 | | -- run 4: index uk_course_translate_course_language + idx_course_version_course_id + idx_enrollment_course_version_id |
| 147 | | |
| 148 | | CREATE INDEX idx_enrollment_course_version_id |
| 149 | | ON enrollment(course_version_id); |
| 150 | | ANALYZE enrollment; |
| 151 | | |
| 152 | | SELECT test_popular_courses(); |
| 153 | | |
| 154 | | -- run 5: all indexes -> uk_course_translate_course_language + idx_course_version_course_id + idx_enrollment_course_version_id + uk_review_enrollment |
| 155 | | |
| 156 | | CREATE UNIQUE INDEX IF NOT EXISTS uk_review_enrollment |
| 157 | | ON review(enrollment_id); |
| 158 | | ANALYZE review; |
| 159 | | |
| 160 | | SELECT test_popular_courses(); |
| 161 | | |
| 162 | | DROP FUNCTION test_popular_courses(); |
| 163 | | }}} |
| 164 | | |
| 165 | | **Сумарно:** |
| 166 | | - **Без индекси:** Seq Scan на сите табели (бавно) |
| 167 | | - **Со индекси:** Index Scan на сите JOIN-ови и WHERE (брзо) |
| 168 | | - **Перформанси:** |
| 169 | | - Без индекси: 51ms |
| 170 | | - Со индекси: 23ms |
| 171 | | - **Забрзување: 2.2 пати** |
| 172 | | |
| | 127 | }}} |
| | 128 | |
| | 129 | ==== Почетна состојба ==== |
| | 130 | |
| | 131 | {{{ |
| | 132 | Sort (cost=37127.27..37129.52 rows=900 width=112) (actual time=150.157..153.299 rows=30 loops=1) |
| | 133 | Sort Key: (((EXTRACT(year FROM e.purchase_date)))::integer) DESC, (((EXTRACT(month FROM e.purchase_date)))::integer) DESC |
| | 134 | Sort Method: quicksort Memory: 26kB |
| | 135 | -> Finalize GroupAggregate (cost=36825.84..37083.11 rows=900 width=112) (actual time=150.115..153.287 rows=30 loops=1) |
| | 136 | Group Key: (EXTRACT(year FROM e.purchase_date)), (EXTRACT(month FROM e.purchase_date)) |
| | 137 | -> Gather Merge (cost=36825.84..37035.86 rows=1800 width=108) (actual time=150.109..153.264 rows=90 loops=1) |
| | 138 | Workers Planned: 2 |
| | 139 | Workers Launched: 2 |
| | 140 | -> Sort (cost=35825.82..35828.07 rows=900 width=108) (actual time=148.317..148.320 rows=30 loops=3) |
| | 141 | Sort Key: (EXTRACT(year FROM e.purchase_date)), (EXTRACT(month FROM e.purchase_date)) |
| | 142 | Sort Method: quicksort Memory: 28kB |
| | 143 | Worker 0: Sort Method: quicksort Memory: 28kB |
| | 144 | Worker 1: Sort Method: quicksort Memory: 28kB |
| | 145 | -> Partial HashAggregate (cost=35765.91..35781.66 rows=900 width=108) (actual time=148.294..148.302 rows=30 loops=3) |
| | 146 | Group Key: EXTRACT(year FROM e.purchase_date), EXTRACT(month FROM e.purchase_date) |
| | 147 | Batches: 1 Memory Usage: 57kB |
| | 148 | Worker 0: Batches: 1 Memory Usage: 57kB |
| | 149 | Worker 1: Batches: 1 Memory Usage: 57kB |
| | 150 | -> Parallel Hash Left Join (cost=15468.58..32432.58 rows=333333 width=81) (actual time=64.563..117.679 rows=266667 loops=3) |
| | 151 | Hash Cond: (e.enrollment_id = p.enrollment_id) |
| | 152 | -> Parallel Seq Scan on enrollment e (cost=0.00..9864.33 rows=333333 width=12) (actual time=0.004..14.058 rows=266667 loops=3) |
| | 153 | -> Parallel Hash (cost=10833.67..10833.67 rows=266633 width=13) (actual time=30.498..30.498 rows=213333 loops=3) |
| | 154 | Buckets: 262144 Batches: 8 Memory Usage: 5856kB |
| | 155 | -> Parallel Seq Scan on payment p (cost=0.00..10833.67 rows=266633 width=13) (actual time=0.016..14.775 rows=213333 loops=3) |
| | 156 | Filter: (payment_status = 'completed'::payment_status) |
| | 157 | Rows Removed by Filter: 53333 |
| | 158 | Planning Time: 0.268 ms |
| | 159 | Execution Time: 153.339 ms |
| | 160 | }}} |
| | 161 | |
| | 162 | Време: '''153,8 ms''' (три пуштања: 157,7 / 150,3 / 153,3 ms). |
| | 163 | |
| | 164 | Од планот се гледа дека `Parallel Seq Scan on enrollment` чита сите 800.000 редови, а `Parallel Seq Scan on payment` чита целата табела и потоа отфрла околу 160.000 редови преку `Filter: payment_status = 'completed'`. Двете страни одат во `Parallel Hash Left Join`. Оттука произлегуваат две идеи: парцијален covering индекс врз `payment`, за да не се читаат воопшто редовите што не се `completed` и `amount` да се земе директно од индексот, и covering индекс врз `enrollment`, за да се овозможи `Index Only Scan` без пристап до heap-от. |
| | 165 | |
| | 166 | ==== Чекор 1, парцијален covering индекс врз payment ==== |
| | 167 | |
| | 168 | {{{ |
| | 169 | CREATE INDEX idx_payment_completed_covering\n ON payment(enrollment_id) INCLUDE (amount)\n WHERE payment_status = 'completed'; |
| | 170 | }}} |
| | 171 | |
| | 172 | {{{ |
| | 173 | Sort (cost=37116.74..37118.99 rows=900 width=112) (actual time=145.784..149.022 rows=30 loops=1) |
| | 174 | Sort Key: (((EXTRACT(year FROM e.purchase_date)))::integer) DESC, (((EXTRACT(month FROM e.purchase_date)))::integer) DESC |
| | 175 | Sort Method: quicksort Memory: 26kB |
| | 176 | -> Finalize GroupAggregate (cost=36815.32..37072.58 rows=900 width=112) (actual time=145.743..149.011 rows=30 loops=1) |
| | 177 | Group Key: (EXTRACT(year FROM e.purchase_date)), (EXTRACT(month FROM e.purchase_date)) |
| | 178 | -> Gather Merge (cost=36815.32..37025.33 rows=1800 width=108) (actual time=145.738..148.989 rows=90 loops=1) |
| | 179 | Workers Planned: 2 |
| | 180 | Workers Launched: 2 |
| | 181 | -> Sort (cost=35815.29..35817.54 rows=900 width=108) (actual time=144.522..144.524 rows=30 loops=3) |
| | 182 | Sort Key: (EXTRACT(year FROM e.purchase_date)), (EXTRACT(month FROM e.purchase_date)) |
| | 183 | Sort Method: quicksort Memory: 28kB |
| | 184 | Worker 0: Sort Method: quicksort Memory: 28kB |
| | 185 | Worker 1: Sort Method: quicksort Memory: 28kB |
| | 186 | -> Partial HashAggregate (cost=35755.38..35771.13 rows=900 width=108) (actual time=144.496..144.505 rows=30 loops=3) |
| | 187 | Group Key: EXTRACT(year FROM e.purchase_date), EXTRACT(month FROM e.purchase_date) |
| | 188 | Batches: 1 Memory Usage: 57kB |
| | 189 | Worker 0: Batches: 1 Memory Usage: 57kB |
| | 190 | Worker 1: Batches: 1 Memory Usage: 57kB |
| | 191 | -> Parallel Hash Left Join (cost=15460.05..32422.05 rows=333333 width=81) (actual time=60.990..113.876 rows=266667 loops=3) |
| | 192 | Hash Cond: (e.enrollment_id = p.enrollment_id) |
| | 193 | -> Parallel Seq Scan on enrollment e (cost=0.00..9864.33 rows=333333 width=12) (actual time=0.018..13.806 rows=266667 loops=3) |
| | 194 | -> Parallel Hash (cost=10833.67..10833.67 rows=266111 width=13) (actual time=28.989..28.989 rows=213333 loops=3) |
| | 195 | Buckets: 262144 Batches: 8 Memory Usage: 5856kB |
| | 196 | -> Parallel Seq Scan on payment p (cost=0.00..10833.67 rows=266111 width=13) (actual time=0.020..14.149 rows=213333 loops=3) |
| | 197 | Filter: (payment_status = 'completed'::payment_status) |
| | 198 | Rows Removed by Filter: 53333 |
| | 199 | Planning Time: 0.327 ms |
| | 200 | Execution Time: 149.062 ms |
| | 201 | }}} |
| | 202 | |
| | 203 | Време: '''153,0 ms''' (три пуштања: 156,5 / 153,5 / 149,1 ms), односно без ефект (+0,5%, во границите на мерната грешка). |
| | 204 | |
| | 205 | Планот е непроменет. Планерот го игнорира индексот и останува на `Parallel Seq Scan on payment`. |
| | 206 | |
| | 207 | ==== Чекор 2, плус covering индекс врз enrollment ==== |
| | 208 | |
| | 209 | {{{ |
| | 210 | CREATE INDEX idx_enrollment_purchase_date_cov\n ON enrollment(purchase_date) INCLUDE (enrollment_id); |
| | 211 | }}} |
| | 212 | |
| | 213 | {{{ |
| | 214 | Sort (cost=37116.74..37118.99 rows=900 width=112) (actual time=162.435..165.607 rows=30 loops=1) |
| | 215 | Sort Key: (((EXTRACT(year FROM e.purchase_date)))::integer) DESC, (((EXTRACT(month FROM e.purchase_date)))::integer) DESC |
| | 216 | Sort Method: quicksort Memory: 26kB |
| | 217 | -> Finalize GroupAggregate (cost=36815.32..37072.58 rows=900 width=112) (actual time=162.379..165.593 rows=30 loops=1) |
| | 218 | Group Key: (EXTRACT(year FROM e.purchase_date)), (EXTRACT(month FROM e.purchase_date)) |
| | 219 | -> Gather Merge (cost=36815.32..37025.33 rows=1800 width=108) (actual time=162.369..165.558 rows=90 loops=1) |
| | 220 | Workers Planned: 2 |
| | 221 | Workers Launched: 2 |
| | 222 | -> Sort (cost=35815.29..35817.54 rows=900 width=108) (actual time=161.332..161.335 rows=30 loops=3) |
| | 223 | Sort Key: (EXTRACT(year FROM e.purchase_date)), (EXTRACT(month FROM e.purchase_date)) |
| | 224 | Sort Method: quicksort Memory: 28kB |
| | 225 | Worker 0: Sort Method: quicksort Memory: 28kB |
| | 226 | Worker 1: Sort Method: quicksort Memory: 28kB |
| | 227 | -> Partial HashAggregate (cost=35755.38..35771.13 rows=900 width=108) (actual time=161.296..161.307 rows=30 loops=3) |
| | 228 | Group Key: EXTRACT(year FROM e.purchase_date), EXTRACT(month FROM e.purchase_date) |
| | 229 | Batches: 1 Memory Usage: 57kB |
| | 230 | Worker 0: Batches: 1 Memory Usage: 57kB |
| | 231 | Worker 1: Batches: 1 Memory Usage: 57kB |
| | 232 | -> Parallel Hash Left Join (cost=15460.05..32422.05 rows=333333 width=81) (actual time=69.057..128.267 rows=266667 loops=3) |
| | 233 | Hash Cond: (e.enrollment_id = p.enrollment_id) |
| | 234 | -> Parallel Seq Scan on enrollment e (cost=0.00..9864.33 rows=333333 width=12) (actual time=0.021..14.344 rows=266667 loops=3) |
| | 235 | -> Parallel Hash (cost=10833.67..10833.67 rows=266111 width=13) (actual time=31.942..31.943 rows=213333 loops=3) |
| | 236 | Buckets: 262144 Batches: 8 Memory Usage: 5856kB |
| | 237 | -> Parallel Seq Scan on payment p (cost=0.00..10833.67 rows=266111 width=13) (actual time=0.023..15.409 rows=213333 loops=3) |
| | 238 | Filter: (payment_status = 'completed'::payment_status) |
| | 239 | Rows Removed by Filter: 53333 |
| | 240 | Planning Time: 0.315 ms |
| | 241 | Execution Time: 165.649 ms |
| | 242 | }}} |
| | 243 | |
| | 244 | Време: '''161,7 ms''' (три пуштања: 162,2 / 157,2 / 165,6 ms), односно забавување од 5,1%. |
| | 245 | |
| | 246 | И вториот индекс е игнориран, и двата scan-а остануваат `Seq Scan`. |
| | 247 | |
| | 248 | ==== Проверка со присилно користење на индексите ==== |
| | 249 | |
| | 250 | За да се потврди дека планерот е во право, индексите се наметнати со `SET enable_seqscan = off`. |
| | 251 | |
| | 252 | {{{ |
| | 253 | Sort (cost=51799.37..51801.62 rows=900 width=112) (actual time=274.477..277.882 rows=30 loops=1) |
| | 254 | Sort Key: (((EXTRACT(year FROM e.purchase_date)))::integer) DESC, (((EXTRACT(month FROM e.purchase_date)))::integer) DESC |
| | 255 | Sort Method: quicksort Memory: 26kB |
| | 256 | -> Finalize GroupAggregate (cost=51497.95..51755.21 rows=900 width=112) (actual time=274.376..277.815 rows=30 loops=1) |
| | 257 | Group Key: (EXTRACT(year FROM e.purchase_date)), (EXTRACT(month FROM e.purchase_date)) |
| | 258 | -> Gather Merge (cost=51497.95..51707.96 rows=1800 width=108) (actual time=274.363..277.783 rows=90 loops=1) |
| | 259 | Workers Planned: 2 |
| | 260 | Workers Launched: 2 |
| | 261 | -> Sort (cost=50497.92..50500.17 rows=900 width=108) (actual time=271.312..271.315 rows=30 loops=3) |
| | 262 | Sort Key: (EXTRACT(year FROM e.purchase_date)), (EXTRACT(month FROM e.purchase_date)) |
| | 263 | Sort Method: quicksort Memory: 28kB |
| | 264 | Worker 0: Sort Method: quicksort Memory: 28kB |
| | 265 | Worker 1: Sort Method: quicksort Memory: 28kB |
| | 266 | -> Partial HashAggregate (cost=50438.01..50453.76 rows=900 width=108) (actual time=271.276..271.285 rows=30 loops=3) |
| | 267 | Group Key: EXTRACT(year FROM e.purchase_date), EXTRACT(month FROM e.purchase_date) |
| | 268 | Batches: 1 Memory Usage: 57kB |
| | 269 | Worker 0: Batches: 1 Memory Usage: 57kB |
| | 270 | Worker 1: Batches: 1 Memory Usage: 57kB |
| | 271 | -> Parallel Hash Left Join (cost=20337.69..47104.68 rows=333333 width=81) (actual time=181.814..237.345 rows=266667 loops=3) |
| | 272 | Hash Cond: (e.enrollment_id = p.enrollment_id) |
| | 273 | -> Parallel Index Only Scan using idx_enrollment_purchase_date_cov on enrollment e (cost=0.42..19669.76 rows=333333 width=12) (actual time=0.085..43.629 rows=266667 loops=3) |
| | 274 | Heap Fetches: 0 |
| | 275 | -> Parallel Hash (cost=15710.87..15710.87 rows=266111 width=13) (actual time=101.237..101.239 rows=213333 loops=3) |
| | 276 | Buckets: 262144 Batches: 8 Memory Usage: 5856kB |
| | 277 | -> Parallel Index Only Scan using idx_payment_completed_covering on payment p (cost=0.42..15710.87 rows=266111 width=13) (actual time=0.101..74.900 rows=213333 loops=3) |
| | 278 | Heap Fetches: 0 |
| | 279 | Planning Time: 1.117 ms |
| | 280 | Execution Time: 278.000 ms |
| | 281 | }}} |
| | 282 | |
| | 283 | Време: 278,0 ms наспроти 153,8 ms, односно '''1,8 пати побавно'''. Планерот донесе точната одлука. |
| | 284 | |
| | 285 | ==== Проверка со WHERE услов ==== |
| | 286 | |
| | 287 | Тврдењето дека индекс нема што да прескокне вреди да се провери директно. Истото query, ограничено на последните околу три месеци, со обичен индекс врз `purchase_date`: |
| | 288 | |
| | 289 | {{{ |
| | 290 | CREATE INDEX idx_enrollment_purchase_date ON enrollment(purchase_date); |
| | 291 | }}} |
| | 292 | |
| | 293 | {{{ |
| | 294 | WHERE e.purchase_date >= DATE '2025-04-01' |
| | 295 | }}} |
| | 296 | |
| | 297 | Тоа зафаќа 70.152 од 800.000 реда, односно околу 8,8%. |
| | 298 | |
| | 299 | {{{ |
| | 300 | Sort (cost=21376.64..21378.89 rows=900 width=112) (actual time=36.787..36.950 rows=3 loops=1) |
| | 301 | Sort Key: (((EXTRACT(year FROM e.purchase_date)))::integer) DESC, (((EXTRACT(month FROM e.purchase_date)))::integer) DESC |
| | 302 | Sort Method: quicksort Memory: 25kB |
| | 303 | -> Finalize GroupAggregate (cost=21075.21..21332.48 rows=900 width=112) (actual time=36.779..36.943 rows=3 loops=1) |
| | 304 | Group Key: (EXTRACT(year FROM e.purchase_date)), (EXTRACT(month FROM e.purchase_date)) |
| | 305 | -> Gather Merge (cost=21075.21..21285.23 rows=1800 width=108) (actual time=36.774..36.938 rows=9 loops=1) |
| | 306 | Workers Planned: 2 |
| | 307 | Workers Launched: 2 |
| | 308 | -> Sort (cost=20075.19..20077.44 rows=900 width=108) (actual time=33.689..33.691 rows=3 loops=3) |
| | 309 | Sort Key: (EXTRACT(year FROM e.purchase_date)), (EXTRACT(month FROM e.purchase_date)) |
| | 310 | Sort Method: quicksort Memory: 25kB |
| | 311 | Worker 0: Sort Method: quicksort Memory: 25kB |
| | 312 | Worker 1: Sort Method: quicksort Memory: 25kB |
| | 313 | -> Partial HashAggregate (cost=20015.28..20031.03 rows=900 width=108) (actual time=33.678..33.682 rows=3 loops=3) |
| | 314 | Group Key: EXTRACT(year FROM e.purchase_date), EXTRACT(month FROM e.purchase_date) |
| | 315 | Batches: 1 Memory Usage: 49kB |
| | 316 | Worker 0: Batches: 1 Memory Usage: 49kB |
| | 317 | Worker 1: Batches: 1 Memory Usage: 49kB |
| | 318 | -> Parallel Hash Right Join (cost=8046.92..19724.57 rows=29071 width=81) (actual time=4.891..31.141 rows=23384 loops=3) |
| | 319 | Hash Cond: (p.enrollment_id = e.enrollment_id) |
| | 320 | -> Parallel Seq Scan on payment p (cost=0.00..10833.67 rows=266145 width=13) (actual time=0.023..12.330 rows=213333 loops=3) |
| | 321 | Filter: (payment_status = 'completed'::payment_status) |
| | 322 | Rows Removed by Filter: 53333 |
| | 323 | -> Parallel Hash (cost=7683.53..7683.53 rows=29071 width=12) (actual time=4.454..4.455 rows=23384 loops=3) |
| | 324 | Buckets: 131072 Batches: 1 Memory Usage: 4384kB |
| | 325 | -> Parallel Bitmap Heap Scan on enrollment e (cost=789.14..7683.53 rows=29071 width=12) (actual time=0.812..2.932 rows=23384 loops=3) |
| | 326 | Recheck Cond: (purchase_date >= '2025-04-01'::date) |
| | 327 | Heap Blocks: exact=548 |
| | 328 | -> Bitmap Index Scan on idx_enrollment_purchase_date (cost=0.00..771.70 rows=69770 width=0) (actual time=0.964..0.964 rows=70152 loops=1) |
| | 329 | Index Cond: (purchase_date >= '2025-04-01'::date) |
| | 330 | Planning Time: 0.318 ms |
| | 331 | Execution Time: 37.000 ms |
| | 332 | }}} |
| | 333 | |
| | 334 | Време: '''38,1 ms''' (три пуштања: 38,9 / 38,4 / 37,0 ms) наспроти 153,8 ms без филтер, односно '''4,0 пати побрзо'''. |
| | 335 | |
| | 336 | И овој пат планерот го искористи индексот: |
| | 337 | |
| | 338 | {{{ |
| | 339 | -> Bitmap Index Scan on idx_enrollment_purchase_date\n (actual time=0.964..0.964 rows=70152 loops=1) |
| | 340 | }}} |
| | 341 | |
| | 342 | Ова е клучната разлика. Истиот индекс што беше игнориран во чекор 1 и 2 сега се користи, и тоа со голема добивка, затоа што сега постои услов што исфрлува редови. Индексот не стана подобар, се промени query-то. |
| | 343 | |
| | 344 | || '''Варијанта''' || '''Редови''' || '''Време''' || |
| | 345 | || без `WHERE` (тековна функција) || 800.000 || 153,8 ms || |
| | 346 | || со филтер по период и индекс || 70.152 || 38,1 ms || |
| | 347 | |
| | 348 | ==== Заклучок ==== |
| | 349 | |
| | 350 | Индексите тука не помагаат, и не постои индекс што би помогнал. Причината е во самата природа на query-то, бидејќи тоа е агрегација врз целата табела без ниту еден `WHERE` услов. За да се пресмета збир по месец, мора да се допре секој ред од `enrollment` и `payment`. |
| | 351 | |
| | 352 | Индексот е корисен кога служи за да се прескокнат редови. Овде нема што да се прескокне, потребни се сите. Во таа ситуација секвенцијалното читање е оптимално, бидејќи чита цели блокови по редослед на дискот, додека index scan би додал индиректност, прво индекс па heap, без да заштеди ниту еден ред. Присилното мерење го докажува тоа емпириски, `Index Only Scan` е побавен од `Seq Scan` за оваа намена. |
| | 353 | |
| | 354 | Вреди да се забележи и дека ова query и онака е најбрзото од четирите. Тоа е така затоа што агрегира во само 30 групи и никогаш не прелева на диск, па тука едноставно нема проблем за решавање. |
| | 355 | |
| | 356 | Најважниот наод е мерењето со `WHERE` услов. Со филтер по период истиот индекс дава 4,0 пати забрзување, што значи дека проблемот не е во индексите, туку во тоа што функцијата секогаш пресметува сè. Ако извештајот во практика се гледа по период, најголемата добивка би дошла од додавање параметри: |
| | 357 | |
| | 358 | {{{ |
| | 359 | dashboard_monthly_totals(p_from DATE, p_to DATE) |
| | 360 | }}} |
| | 361 | |
| | 362 | === dashboard_monthly_courses() === |
| | 363 | |
| | 364 | Приход и запишувања по месец, разложено по курс и верзија. Резултатот има 86.400 реда. Ова е најбавната од четирите функции. |
| | 365 | |
| | 366 | ==== Дефиниција ==== |
| | 367 | |
| | 368 | {{{ |
| | 369 | -- Monthly course-specific |
| | 370 | CREATE OR REPLACE FUNCTION dashboard_monthly_courses() |
| | 371 | RETURNS TABLE ( |
| | 372 | year INTEGER, |
| | 373 | month INTEGER, |
| | 374 | course_id INTEGER, |
| | 375 | course_name TEXT, |
| | 376 | course_description TEXT, |
| | 377 | course_difficulty TEXT, |
| | 378 | course_price NUMERIC, |
| | 379 | version_number INTEGER, |
| | 380 | is_version_active BOOLEAN, |
| | 381 | total_paid_enrollments BIGINT, |
| | 382 | total_students BIGINT, |
| | 383 | total_revenue NUMERIC, |
| | 384 | total_reviews BIGINT, |
| | 385 | average_rating NUMERIC |
| | 386 | ) AS $$ |
| | 387 | #variable_conflict use_column |
| | 388 | BEGIN |
| | 389 | RETURN QUERY |
| | 390 | SELECT |
| | 391 | EXTRACT(YEAR FROM p.payment_date)::INTEGER AS year, |
| | 392 | EXTRACT(MONTH FROM p.payment_date)::INTEGER AS month, |
| | 393 | c.course_id::INTEGER AS course_id, |
| | 394 | ct.title_short::TEXT AS course_name, |
| | 395 | ct.description_short::TEXT AS course_description, |
| | 396 | c.difficulty::TEXT AS course_difficulty, |
| | 397 | c.price::NUMERIC AS course_price, |
| | 398 | cv.version_number::INTEGER AS version_number, |
| | 399 | cv.is_active::BOOLEAN AS is_version_active, |
| | 400 | COUNT(e.enrollment_id)::BIGINT AS total_paid_enrollments, |
| | 401 | COUNT(DISTINCT e.user_id)::BIGINT AS total_students, |
| | 402 | SUM(p.amount)::NUMERIC AS total_revenue, |
| | 403 | COUNT(r.review_id)::BIGINT AS total_reviews, |
| | 404 | COALESCE(AVG(r.rating), 0)::NUMERIC AS average_rating |
| | 405 | FROM course c |
| | 406 | JOIN course_translate ct ON c.course_id = ct.course_id |
| | 407 | JOIN language l ON l.id = ct.language_id |
| | 408 | JOIN course_version cv ON c.course_id = cv.course_id |
| | 409 | JOIN enrollment e ON cv.course_version_id = e.course_version_id |
| | 410 | JOIN payment p ON e.enrollment_id = p.enrollment_id AND p.payment_status = 'completed' |
| | 411 | LEFT JOIN review r ON e.enrollment_id = r.enrollment_id |
| | 412 | GROUP BY |
| | 413 | EXTRACT(YEAR FROM p.payment_date), |
| | 414 | EXTRACT(MONTH FROM p.payment_date), |
| | 415 | c.course_id, ct.course_translate_id, cv.course_version_id |
| | 416 | ORDER BY year DESC, month DESC, total_revenue DESC, total_students DESC; |
| | 417 | END; |
| | 418 | $$ LANGUAGE plpgsql; |
| | 419 | }}} |
| | 420 | |
| | 421 | ==== Почетна состојба ==== |
| | 422 | |
| | 423 | {{{ |
| | 424 | Sort (cost=1216174.15..1220973.55 rows=1919760 width=331) (actual time=3403.219..3423.748 rows=86400 loops=1) |
| | 425 | Sort Key: (((EXTRACT(year FROM p.payment_date)))::integer) DESC, (((EXTRACT(month FROM p.payment_date)))::integer) DESC, (sum(p.amount)) DESC, (count(DISTINCT e.user_id)) DESC |
| | 426 | Sort Method: external merge Disk: 14728kB |
| | 427 | -> GroupAggregate (cost=157773.34..425268.25 rows=1919760 width=331) (actual time=488.509..3344.444 rows=86400 loops=1) |
| | 428 | Group Key: cv.course_version_id, (EXTRACT(year FROM p.payment_date)), (EXTRACT(month FROM p.payment_date)), c.course_id, ct.course_translate_id |
| | 429 | -> Incremental Sort (cost=157773.34..305283.25 rows=1919760 width=192) (actual time=488.481..3119.040 rows=1920000 loops=1) |
| | 430 | Sort Key: cv.course_version_id, (EXTRACT(year FROM p.payment_date)), (EXTRACT(month FROM p.payment_date)), c.course_id, ct.course_translate_id, e.user_id |
| | 431 | Presorted Key: cv.course_version_id |
| | 432 | Full-sort Groups: 5400 Sort Method: quicksort Average Memory: 35kB Peak Memory: 35kB |
| | 433 | Pre-sorted Groups: 5400 Sort Method: quicksort Average Memory: 67kB Peak Memory: 68kB |
| | 434 | -> Merge Join (cost=157752.78..201287.46 rows=1919760 width=192) (actual time=488.170..857.077 rows=1920000 loops=1) |
| | 435 | Merge Cond: (cv.course_version_id = e.course_version_id) |
| | 436 | -> Nested Loop (cost=1.00..3496.80 rows=18000 width=91) (actual time=173.855..190.556 rows=18000 loops=1) |
| | 437 | -> Nested Loop (cost=0.86..3091.22 rows=18000 width=99) (actual time=173.848..187.208 rows=18000 loops=1) |
| | 438 | Join Filter: (c.course_id = ct.course_id) |
| | 439 | -> Nested Loop (cost=0.57..1142.36 rows=6000 width=38) (actual time=173.836..178.986 rows=6000 loops=1) |
| | 440 | -> Index Scan using course_version_pkey on course_version cv (cost=0.28..355.58 rows=6000 width=21) (actual time=0.006..1.836 rows=6000 loops=1) |
| | 441 | -> Memoize (cost=0.29..0.33 rows=1 width=17) (actual time=0.029..0.029 rows=1 loops=6000) |
| | 442 | Cache Key: cv.course_id |
| | 443 | Cache Mode: logical |
| | 444 | Hits: 4000 Misses: 2000 Evictions: 0 Overflows: 0 Memory Usage: 237kB |
| | 445 | -> Index Scan using course_pkey on course c (cost=0.28..0.32 rows=1 width=17) (actual time=0.001..0.001 rows=1 loops=2000) |
| | 446 | Index Cond: (course_id = cv.course_id) |
| | 447 | -> Memoize (cost=0.29..0.81 rows=3 width=77) (actual time=0.000..0.001 rows=3 loops=6000) |
| | 448 | Cache Key: cv.course_id |
| | 449 | Cache Mode: logical |
| | 450 | Hits: 4000 Misses: 2000 Evictions: 0 Overflows: 0 Memory Usage: 797kB |
| | 451 | -> Index Scan using uq_course_translate on course_translate ct (cost=0.28..0.80 rows=3 width=77) (actual time=0.001..0.002 rows=3 loops=2000) |
| | 452 | Index Cond: (course_id = cv.course_id) |
| | 453 | -> Memoize (cost=0.14..0.16 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=18000) |
| | 454 | Cache Key: ct.language_id |
| | 455 | Cache Mode: logical |
| | 456 | Hits: 17997 Misses: 3 Evictions: 0 Overflows: 0 Memory Usage: 1kB |
| | 457 | -> Index Only Scan using language_pkey on language l (cost=0.13..0.15 rows=1 width=8) (actual time=0.002..0.002 rows=1 loops=3) |
| | 458 | Index Cond: (id = ct.language_id) |
| | 459 | Heap Fetches: 0 |
| | 460 | -> Materialize (cost=157751.13..160950.73 rows=639920 width=45) (actual time=314.289..406.363 rows=1919998 loops=1) |
| | 461 | -> Sort (cost=157751.13..159350.93 rows=639920 width=45) (actual time=314.287..347.268 rows=640000 loops=1) |
| | 462 | Sort Key: e.course_version_id |
| | 463 | Sort Method: external merge Disk: 29944kB |
| | 464 | -> Merge Left Join (cost=1.80..76351.25 rows=639920 width=45) (actual time=0.043..237.686 rows=640000 loops=1) |
| | 465 | Merge Cond: (e.enrollment_id = r.enrollment_id) |
| | 466 | -> Merge Join (cost=1.38..66767.19 rows=639920 width=33) (actual time=0.031..183.912 rows=640000 loops=1) |
| | 467 | Merge Cond: (e.enrollment_id = p.enrollment_id) |
| | 468 | -> Index Scan using enrollment_pkey on enrollment e (cost=0.42..27318.42 rows=800000 width=24) (actual time=0.017..55.353 rows=800000 loops=1) |
| | 469 | -> Index Scan using uq_payment_enrollment on payment p (cost=0.42..29454.42 rows=639920 width=17) (actual time=0.010..59.537 rows=640000 loops=1) |
| | 470 | Filter: (payment_status = 'completed'::payment_status) |
| | 471 | Rows Removed by Filter: 160000 |
| | 472 | -> Index Scan using uq_review_enrollment on review r (cost=0.42..6305.42 rows=160000 width=20) (actual time=0.009..12.521 rows=160000 loops=1) |
| | 473 | Planning Time: 1.646 ms |
| | 474 | JIT: |
| | 475 | Functions: 53 |
| | 476 | Options: Inlining true, Optimization true, Expressions true, Deforming true |
| | 477 | Timing: Generation 0.945 ms (Deform 0.511 ms), Inlining 30.437 ms, Optimization 75.801 ms, Emission 67.586 ms, Total 174.768 ms |
| | 478 | Execution Time: 3438.197 ms |
| | 479 | }}} |
| | 480 | |
| | 481 | Време: '''3434,4 ms''' (три пуштања: 3446,3 / 3418,8 / 3438,2 ms). |
| | 482 | |
| | 483 | Планот покажува неколку работи. `Merge Join` враќа 1.920.000 реда, тројно повеќе од очекуваното. `Incremental Sort` трае од 488 ms до 3119 ms, што значи дека околу 2,6 секунди, односно приблизно 76% од целото време, се троши на едно сортирање. Сортирањата се прелеваат на диск, `external merge Disk: 29944kB` и `external merge Disk: 14728kB`. Во клучот за сортирање влегува и `e.user_id`, поради `COUNT(DISTINCT e.user_id)`. Конечно, `enrollment.course_version_id` нема индекс. |
| | 484 | |
| | 485 | Оттука произлегуваат три идеи: индекс врз непокриениот foreign key `enrollment.course_version_id`, композитен индекс `(course_version_id, user_id)` што би можел да го понуди редоследот што `Incremental Sort` го бара, и парцијален covering индекс врз `payment` за наплатените плаќања. |
| | 486 | |
| | 487 | ==== Чекор 1, индекс врз непокриениот foreign key ==== |
| | 488 | |
| | 489 | {{{ |
| | 490 | CREATE INDEX idx_enrollment_course_version_id\n ON enrollment(course_version_id); |
| | 491 | }}} |
| | 492 | |
| | 493 | {{{ |
| | 494 | Sort (cost=1213871.37..1218661.37 rows=1916001 width=331) (actual time=3384.601..3405.332 rows=86400 loops=1) |
| | 495 | Sort Key: (((EXTRACT(year FROM p.payment_date)))::integer) DESC, (((EXTRACT(month FROM p.payment_date)))::integer) DESC, (sum(p.amount)) DESC, (count(DISTINCT e.user_id)) DESC |
| | 496 | Sort Method: external merge Disk: 14728kB |
| | 497 | -> GroupAggregate (cost=157587.52..424539.86 rows=1916001 width=331) (actual time=493.498..3324.347 rows=86400 loops=1) |
| | 498 | Group Key: cv.course_version_id, (EXTRACT(year FROM p.payment_date)), (EXTRACT(month FROM p.payment_date)), c.course_id, ct.course_translate_id |
| | 499 | -> Incremental Sort (cost=157587.52..304789.79 rows=1916001 width=192) (actual time=493.453..3100.893 rows=1920000 loops=1) |
| | 500 | Sort Key: cv.course_version_id, (EXTRACT(year FROM p.payment_date)), (EXTRACT(month FROM p.payment_date)), c.course_id, ct.course_translate_id, e.user_id |
| | 501 | Presorted Key: cv.course_version_id |
| | 502 | Full-sort Groups: 5400 Sort Method: quicksort Average Memory: 35kB Peak Memory: 35kB |
| | 503 | Pre-sorted Groups: 5400 Sort Method: quicksort Average Memory: 67kB Peak Memory: 68kB |
| | 504 | -> Merge Join (cost=157567.00..201024.48 rows=1916001 width=192) (actual time=493.176..843.628 rows=1920000 loops=1) |
| | 505 | Merge Cond: (cv.course_version_id = e.course_version_id) |
| | 506 | -> Nested Loop (cost=1.00..3496.80 rows=18000 width=91) (actual time=181.119..197.409 rows=18000 loops=1) |
| | 507 | -> Nested Loop (cost=0.86..3091.22 rows=18000 width=99) (actual time=181.113..194.116 rows=18000 loops=1) |
| | 508 | Join Filter: (c.course_id = ct.course_id) |
| | 509 | -> Nested Loop (cost=0.57..1142.36 rows=6000 width=38) (actual time=181.097..186.273 rows=6000 loops=1) |
| | 510 | -> Index Scan using course_version_pkey on course_version cv (cost=0.28..355.58 rows=6000 width=21) (actual time=0.006..1.839 rows=6000 loops=1) |
| | 511 | -> Memoize (cost=0.29..0.33 rows=1 width=17) (actual time=0.031..0.031 rows=1 loops=6000) |
| | 512 | Cache Key: cv.course_id |
| | 513 | Cache Mode: logical |
| | 514 | Hits: 4000 Misses: 2000 Evictions: 0 Overflows: 0 Memory Usage: 237kB |
| | 515 | -> Index Scan using course_pkey on course c (cost=0.28..0.32 rows=1 width=17) (actual time=0.001..0.001 rows=1 loops=2000) |
| | 516 | Index Cond: (course_id = cv.course_id) |
| | 517 | -> Memoize (cost=0.29..0.81 rows=3 width=77) (actual time=0.000..0.001 rows=3 loops=6000) |
| | 518 | Cache Key: cv.course_id |
| | 519 | Cache Mode: logical |
| | 520 | Hits: 4000 Misses: 2000 Evictions: 0 Overflows: 0 Memory Usage: 797kB |
| | 521 | -> Index Scan using uq_course_translate on course_translate ct (cost=0.28..0.80 rows=3 width=77) (actual time=0.001..0.002 rows=3 loops=2000) |
| | 522 | Index Cond: (course_id = cv.course_id) |
| | 523 | -> Memoize (cost=0.14..0.16 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=18000) |
| | 524 | Cache Key: ct.language_id |
| | 525 | Cache Mode: logical |
| | 526 | Hits: 17997 Misses: 3 Evictions: 0 Overflows: 0 Memory Usage: 1kB |
| | 527 | -> Index Only Scan using language_pkey on language l (cost=0.13..0.15 rows=1 width=8) (actual time=0.002..0.002 rows=1 loops=3) |
| | 528 | Index Cond: (id = ct.language_id) |
| | 529 | Heap Fetches: 0 |
| | 530 | -> Materialize (cost=157565.99..160759.33 rows=638667 width=45) (actual time=312.034..404.507 rows=1919998 loops=1) |
| | 531 | -> Sort (cost=157565.99..159162.66 rows=638667 width=45) (actual time=312.032..345.703 rows=640000 loops=1) |
| | 532 | Sort Key: e.course_version_id |
| | 533 | Sort Method: external merge Disk: 29944kB |
| | 534 | -> Merge Left Join (cost=4.28..76334.47 rows=638667 width=45) (actual time=0.046..236.700 rows=640000 loops=1) |
| | 535 | Merge Cond: (e.enrollment_id = r.enrollment_id) |
| | 536 | -> Merge Join (cost=3.86..66755.26 rows=638667 width=33) (actual time=0.033..183.695 rows=640000 loops=1) |
| | 537 | Merge Cond: (e.enrollment_id = p.enrollment_id) |
| | 538 | -> Index Scan using enrollment_pkey on enrollment e (cost=0.42..27318.42 rows=800000 width=24) (actual time=0.018..55.760 rows=800000 loops=1) |
| | 539 | -> Index Scan using uq_payment_enrollment on payment p (cost=0.42..29454.42 rows=638667 width=17) (actual time=0.011..59.935 rows=640000 loops=1) |
| | 540 | Filter: (payment_status = 'completed'::payment_status) |
| | 541 | Rows Removed by Filter: 160000 |
| | 542 | -> Index Scan using uq_review_enrollment on review r (cost=0.42..6305.42 rows=160000 width=20) (actual time=0.010..12.617 rows=160000 loops=1) |
| | 543 | Planning Time: 1.632 ms |
| | 544 | JIT: |
| | 545 | Functions: 53 |
| | 546 | Options: Inlining true, Optimization true, Expressions true, Deforming true |
| | 547 | Timing: Generation 0.834 ms (Deform 0.414 ms), Inlining 34.467 ms, Optimization 77.756 ms, Emission 68.858 ms, Total 181.915 ms |
| | 548 | Execution Time: 3419.300 ms |
| | 549 | }}} |
| | 550 | |
| | 551 | Време: '''3425,5 ms''' (три пуштања: 3435,3 / 3422,0 / 3419,3 ms), односно без ефект (+0,3%, во границите на мерната грешка). |
| | 552 | |
| | 553 | `external merge Disk: 29944kB` е сè уште тука, индексот не го отстрани сортирањето. |
| | 554 | |
| | 555 | ==== Чекор 2, композитен индекс за редослед и парцијален индекс врз payment ==== |
| | 556 | |
| | 557 | {{{ |
| | 558 | CREATE INDEX idx_enrollment_cv_user\n ON enrollment(course_version_id, user_id) INCLUDE (enrollment_id);\n\nCREATE INDEX idx_payment_completed_cov2\n ON payment(enrollment_id) INCLUDE (amount, payment_date)\n WHERE payment_status = 'completed'; |
| | 559 | }}} |
| | 560 | |
| | 561 | {{{ |
| | 562 | Sort (cost=1210572.31..1215378.11 rows=1922319 width=331) (actual time=3346.915..3366.914 rows=86400 loops=1) |
| | 563 | Sort Key: (((EXTRACT(year FROM p.payment_date)))::integer) DESC, (((EXTRACT(month FROM p.payment_date)))::integer) DESC, (sum(p.amount)) DESC, (count(DISTINCT e.user_id)) DESC |
| | 564 | Sort Method: external merge Disk: 14728kB |
| | 565 | -> GroupAggregate (cost=150730.70..418596.88 rows=1922319 width=331) (actual time=422.918..3285.159 rows=86400 loops=1) |
| | 566 | Group Key: cv.course_version_id, (EXTRACT(year FROM p.payment_date)), (EXTRACT(month FROM p.payment_date)), c.course_id, ct.course_translate_id |
| | 567 | -> Incremental Sort (cost=150730.70..298451.94 rows=1922319 width=192) (actual time=422.888..3057.450 rows=1920000 loops=1) |
| | 568 | Sort Key: cv.course_version_id, (EXTRACT(year FROM p.payment_date)), (EXTRACT(month FROM p.payment_date)), c.course_id, ct.course_translate_id, e.user_id |
| | 569 | Presorted Key: cv.course_version_id |
| | 570 | Full-sort Groups: 5400 Sort Method: quicksort Average Memory: 35kB Peak Memory: 35kB |
| | 571 | Pre-sorted Groups: 5400 Sort Method: quicksort Average Memory: 67kB Peak Memory: 68kB |
| | 572 | -> Merge Join (cost=150710.10..194299.21 rows=1922319 width=192) (actual time=422.594..786.566 rows=1920000 loops=1) |
| | 573 | Merge Cond: (cv.course_version_id = e.course_version_id) |
| | 574 | -> Nested Loop (cost=1.00..3496.80 rows=18000 width=91) (actual time=161.040..177.890 rows=18000 loops=1) |
| | 575 | -> Nested Loop (cost=0.86..3091.22 rows=18000 width=99) (actual time=161.033..174.454 rows=18000 loops=1) |
| | 576 | Join Filter: (c.course_id = ct.course_id) |
| | 577 | -> Nested Loop (cost=0.57..1142.36 rows=6000 width=38) (actual time=161.022..166.381 rows=6000 loops=1) |
| | 578 | -> Index Scan using course_version_pkey on course_version cv (cost=0.28..355.58 rows=6000 width=21) (actual time=0.006..1.895 rows=6000 loops=1) |
| | 579 | -> Memoize (cost=0.29..0.33 rows=1 width=17) (actual time=0.027..0.027 rows=1 loops=6000) |
| | 580 | Cache Key: cv.course_id |
| | 581 | Cache Mode: logical |
| | 582 | Hits: 4000 Misses: 2000 Evictions: 0 Overflows: 0 Memory Usage: 237kB |
| | 583 | -> Index Scan using course_pkey on course c (cost=0.28..0.32 rows=1 width=17) (actual time=0.001..0.001 rows=1 loops=2000) |
| | 584 | Index Cond: (course_id = cv.course_id) |
| | 585 | -> Memoize (cost=0.29..0.81 rows=3 width=77) (actual time=0.000..0.001 rows=3 loops=6000) |
| | 586 | Cache Key: cv.course_id |
| | 587 | Cache Mode: logical |
| | 588 | Hits: 4000 Misses: 2000 Evictions: 0 Overflows: 0 Memory Usage: 797kB |
| | 589 | -> Index Scan using uq_course_translate on course_translate ct (cost=0.28..0.80 rows=3 width=77) (actual time=0.001..0.002 rows=3 loops=2000) |
| | 590 | Index Cond: (course_id = cv.course_id) |
| | 591 | -> Memoize (cost=0.14..0.16 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=18000) |
| | 592 | Cache Key: ct.language_id |
| | 593 | Cache Mode: logical |
| | 594 | Hits: 17997 Misses: 3 Evictions: 0 Overflows: 0 Memory Usage: 1kB |
| | 595 | -> Index Only Scan using language_pkey on language l (cost=0.13..0.15 rows=1 width=8) (actual time=0.002..0.002 rows=1 loops=3) |
| | 596 | Index Cond: (id = ct.language_id) |
| | 597 | Heap Fetches: 0 |
| | 598 | -> Materialize (cost=150709.09..153912.96 rows=640773 width=45) (actual time=261.530..354.982 rows=1919998 loops=1) |
| | 599 | -> Sort (cost=150709.09..152311.03 rows=640773 width=45) (actual time=261.528..293.805 rows=640000 loops=1) |
| | 600 | Sort Key: e.course_version_id |
| | 601 | Sort Method: external merge Disk: 29944kB |
| | 602 | -> Merge Left Join (cost=2.33..69196.29 rows=640773 width=45) (actual time=0.026..190.549 rows=640000 loops=1) |
| | 603 | Merge Cond: (e.enrollment_id = r.enrollment_id) |
| | 604 | -> Merge Join (cost=1.91..59607.51 rows=640773 width=33) (actual time=0.017..141.685 rows=640000 loops=1) |
| | 605 | Merge Cond: (e.enrollment_id = p.enrollment_id) |
| | 606 | -> Index Scan using enrollment_pkey on enrollment e (cost=0.42..27318.42 rows=800000 width=24) (actual time=0.008..44.448 rows=800000 loops=1) |
| | 607 | -> Index Only Scan using idx_payment_completed_cov2 on payment p (cost=0.42..22280.02 rows=640773 width=17) (actual time=0.006..27.571 rows=640000 loops=1) |
| | 608 | Heap Fetches: 0 |
| | 609 | -> Index Scan using uq_review_enrollment on review r (cost=0.42..6305.42 rows=160000 width=20) (actual time=0.006..9.342 rows=160000 loops=1) |
| | 610 | Planning Time: 1.767 ms |
| | 611 | JIT: |
| | 612 | Functions: 49 |
| | 613 | Options: Inlining true, Optimization true, Expressions true, Deforming true |
| | 614 | Timing: Generation 0.946 ms (Deform 0.461 ms), Inlining 30.944 ms, Optimization 68.381 ms, Emission 61.678 ms, Total 161.949 ms |
| | 615 | Execution Time: 3381.984 ms |
| | 616 | }}} |
| | 617 | |
| | 618 | Време: '''3360,9 ms''' (три пуштања: 3354,5 / 3346,1 / 3382,0 ms), односно маргинално забрзување од 2,1%. |
| | 619 | |
| | 620 | Овде има мал напредок и тој е видлив во планот, бидејќи парцијалниот индекс навистина се користи: |
| | 621 | |
| | 622 | {{{ |
| | 623 | -> Index Only Scan using idx_payment_completed_cov2 on payment p\n (actual time=0.006..27.571 rows=640000 loops=1) |
| | 624 | }}} |
| | 625 | |
| | 626 | Наспроти оригиналниот `Index Scan ... (actual time=0.010..59.537)`, тој јазол стана 2,2 пати побрз. Но бидејќи носи само околу 30 ms од вкупно околу 3400 ms, крајниот ефект е околу 2%. `Incremental Sort` и понатаму троши 3057 ms и останува недопрен. |
| | 627 | |
| | 628 | ==== Проверка со work_mem ==== |
| | 629 | |
| | 630 | Бидејќи сортирањата се прелеваат на диск, тестирано е и подигање на `work_mem` од 4 MB на 256 MB. |
| | 631 | |
| | 632 | {{{ |
| | 633 | Sort Method: quicksort Memory: 18260kB (наместо external merge Disk: 14728kB)\nSort Method: quicksort Memory: 62077kB (наместо external merge Disk: 29944kB)\nExecution Time: 3388.270 ms |
| | 634 | }}} |
| | 635 | |
| | 636 | Прелевањето на диск целосно исчезна, но времето остана речиси исто, 3388 ms наспроти 3434,4 ms. Заклучокот е дека тесното грло не е диск I/O, бидејќи оперативниот систем и онака ги држеше тие привремени датотеки во page cache. Проблемот е чисто процесорски, сортирање и агрегирање на 1,92 милиони редови. |
| | 637 | |
| | 638 | ==== Од каде доаѓаат 1.920.000 редови ==== |
| | 639 | |
| | 640 | Query-то содржи join кон `language` без филтер по јазик: |
| | 641 | |
| | 642 | {{{ |
| | 643 | JOIN language l ON l.id = ct.language_id |
| | 644 | }}} |
| | 645 | |
| | 646 | Во базата има 3 јазици, `mk`, `en` и `sq`, и точно 3 преводи по курс, па секое запишување се множи со 3. Од 640.000 платени запишувања се добиваат 1.920.000 реда во join-от, а излезот е 86.400 реда наместо 28.800. Со додаден филтер за јазик: |
| | 647 | |
| | 648 | {{{ |
| | 649 | JOIN language l ON l.id = ct.language_id AND l.value = 'en' |
| | 650 | }}} |
| | 651 | |
| | 652 | времето паѓа на '''1055,3 ms''', односно '''3,3 пати побрзо''', и излезот станува 28.800 реда. |
| | 653 | |
| | 654 | Истото важи и за `dashboard_course_performance()`. И таа функција намерно враќа резултат за сите јазици, и таму трите јазика чинат приблизно двојно повеќе време. |
| | 655 | |
| | 656 | Ова не е предлог да се смени функцијата. Враќањето на сите јазици е намерна одлука, а филтрирањето би го променило резултатот, од 86.400 на 28.800 реда. Мерењето е наведено само за да се види каде оди времето. Ако некогаш затреба извештај само за еден јазик, најефтино е јазикот да биде параметар на функцијата, наместо филтрирање врз готовиот резултат, бидејќи тогаш трите пати повеќе редови никогаш не влегуваат во агрегацијата. |
| | 657 | |
| | 658 | ==== Заклучок ==== |
| | 659 | |
| | 660 | Индексите донесоа 2,1%, а вистинскиот проблем е на друго место. Тесното грло е `Incremental Sort` врз 1,92 милиони редови, кој сам по себе троши околу три четвртини од времето, и тоа сортирање не може да се замени со индекс поради две причини. |
| | 661 | |
| | 662 | Првата е што клучот за сортирање се состои од столбови од повеќе различни табели, `cv.course_version_id`, `c.course_id`, `ct.course_translate_id` и `e.user_id`, плус пресметани изрази со `EXTRACT(...)`. Индексот дава подреденост само во рамките на една табела, па не постои индекс што би го дал тој редослед врз резултатот од join-от. Втората е што `COUNT(DISTINCT e.user_id)` го присилува `user_id` да влезе во клучот за сортирање, бидејќи PostgreSQL мора да ги групира вредностите за да ги изброи уникатните. |
| | 663 | |
| | 664 | Затоа поредокот на ефикасност овде е следниот: |
| | 665 | |
| | 666 | || '''Интервенција''' || '''Време''' || '''Добивка''' || |
| | 667 | || почетна состојба || 3434,4 ms || — || |
| | 668 | || индекси, чекор 1 и 2 || 3360,9 ms || 2,1% || |
| | 669 | || `work_mem` 4 MB на 256 MB || 3388,3 ms || околу 1% || |
| | 670 | || филтер по јазик, менува резултат || 1055,3 ms || 69% || |
| | 671 | |
| | 672 | Со други зборови, сите индекси заедно донесоа околу 2%, додека бројот на редови што влегуваат во агрегацијата чини три пати побавно query. Кога планот покажува дека проблемот е бројот на редови, решението е да се намали тој број, а не да се додаваат индекси. |
| | 673 | |
| | 674 | === dashboard_course_performance() === |
| | 675 | |
| | 676 | Изведба на секој курс и секоја негова верзија, за сите времиња и за сите јазици. Функцијата намерно не филтрира по јазик. `course_translate` содржи по 3 преводи за секој курс, `mk`, `en` и `sq`, па резултатот содржи по еден ред за секој превод на секоја верзија на курс, вкупно 18.000 реда од 6.000 верзии. |
| | 677 | |
| | 678 | ==== Дефиниција ==== |
| | 679 | |
| | 680 | {{{ |
| | 681 | -- All time course specific, for each course version |
| | 682 | CREATE OR REPLACE FUNCTION dashboard_course_performance() |
| | 683 | RETURNS TABLE ( |
| | 684 | course_id INTEGER, |
| | 685 | course_name TEXT, |
| | 686 | course_description TEXT, |
| | 687 | course_difficulty TEXT, |
| | 688 | course_price NUMERIC, |
| | 689 | version_number INTEGER, |
| | 690 | is_version_active BOOLEAN, |
| | 691 | total_enrollments BIGINT, |
| | 692 | paid_enrollments BIGINT, |
| | 693 | trial_enrollments BIGINT, |
| | 694 | total_completions BIGINT, |
| | 695 | total_students BIGINT, |
| | 696 | total_revenue NUMERIC, |
| | 697 | average_rating NUMERIC, |
| | 698 | total_reviews BIGINT, |
| | 699 | avg_days_to_complete INTEGER, |
| | 700 | completion_rate_percentage NUMERIC, |
| | 701 | avg_lecture_completion_percentage NUMERIC |
| | 702 | ) AS $$ |
| | 703 | #variable_conflict use_column |
| | 704 | BEGIN |
| | 705 | RETURN QUERY |
| | 706 | WITH lecture_progress AS ( |
| | 707 | SELECT ucp.enrollment_id, |
| | 708 | COUNT(*) FILTER (WHERE ucp.is_completed) * 100.0 / COUNT(*) AS completed_percentage |
| | 709 | FROM user_course_progress ucp |
| | 710 | GROUP BY ucp.enrollment_id |
| | 711 | ) |
| | 712 | SELECT |
| | 713 | c.course_id::INTEGER AS course_id, |
| | 714 | ct.title_short::TEXT AS course_name, |
| | 715 | ct.description_short::TEXT AS course_description, |
| | 716 | c.difficulty::TEXT AS course_difficulty, |
| | 717 | c.price::NUMERIC AS course_price, |
| | 718 | cv.version_number::INTEGER AS version_number, |
| | 719 | cv.is_active::BOOLEAN AS is_version_active, |
| | 720 | COUNT(e.enrollment_id)::BIGINT AS total_enrollments, |
| | 721 | COUNT(CASE WHEN p.payment_status = 'completed' THEN e.enrollment_id END)::BIGINT AS paid_enrollments, |
| | 722 | COUNT(CASE WHEN p.payment_id IS NULL THEN e.enrollment_id END)::BIGINT AS trial_enrollments, |
| | 723 | COUNT(CASE WHEN e.completion_date IS NOT NULL THEN e.enrollment_id END)::BIGINT AS total_completions, |
| | 724 | COUNT(DISTINCT e.user_id)::BIGINT AS total_students, |
| | 725 | COALESCE(SUM(CASE WHEN p.payment_status = 'completed' THEN p.amount ELSE 0 END), 0)::NUMERIC AS total_revenue, |
| | 726 | COALESCE(AVG(r.rating), 0)::NUMERIC AS average_rating, |
| | 727 | COUNT(r.review_id)::BIGINT AS total_reviews, |
| | 728 | ROUND(AVG(CASE WHEN e.completion_date IS NOT NULL |
| | 729 | THEN (e.completion_date - e.activation_date) |
| | 730 | END), 0)::INTEGER AS avg_days_to_complete, |
| | 731 | COALESCE( |
| | 732 | ROUND(100.0 * COUNT(CASE WHEN e.completion_date IS NOT NULL THEN e.enrollment_id END)::NUMERIC |
| | 733 | / NULLIF(COUNT(e.enrollment_id), 0), 2), |
| | 734 | 0)::NUMERIC AS completion_rate_percentage, |
| | 735 | COALESCE(ROUND(AVG(lp.completed_percentage), 2), 0)::NUMERIC AS avg_lecture_completion_percentage |
| | 736 | FROM course c |
| | 737 | JOIN course_translate ct ON c.course_id = ct.course_id |
| | 738 | JOIN language l ON l.id = ct.language_id |
| | 739 | JOIN course_version cv ON c.course_id = cv.course_id |
| | 740 | LEFT JOIN enrollment e ON cv.course_version_id = e.course_version_id -- include versions with zero enrollments |
| | 741 | LEFT JOIN payment p ON e.enrollment_id = p.enrollment_id -- include enrollments without payments |
| | 742 | LEFT JOIN review r ON e.enrollment_id = r.enrollment_id -- include enrollments without reviews |
| | 743 | LEFT JOIN lecture_progress lp ON lp.enrollment_id = e.enrollment_id -- include enrollments with no progress rows |
| | 744 | GROUP BY c.course_id, ct.course_translate_id, cv.course_version_id |
| | 745 | ORDER BY total_revenue DESC, completion_rate_percentage DESC; |
| | 746 | END; |
| | 747 | $$ LANGUAGE plpgsql; |
| | 748 | }}} |
| | 749 | |
| | 750 | ==== Почетна состојба ==== |
| | 751 | |
| | 752 | {{{ |
| | 753 | Sort (cost=1738812.79..1744812.79 rows=2400000 width=351) (actual time=2946.780..2947.192 rows=18000 loops=1) |
| | 754 | Sort Key: (COALESCE(sum(CASE WHEN (p.payment_status = 'completed'::payment_status) THEN p.amount ELSE '0'::numeric END), '0'::numeric)) DESC, (COALESCE(round(((100.0 * (count(CASE WHEN (e.completion_date IS NOT NULL) THEN e.enrollment_id ELSE NULL::bigint END))::numeric) / (NULLIF(count(e.enrollment_id), 0))::numeric), 2), '0'::numeric)) DESC |
| | 755 | Sort Method: quicksort Memory: 4026kB |
| | 756 | -> GroupAggregate (cost=325503.28..713378.56 rows=2400000 width=351) (actual time=936.469..2937.854 rows=18000 loops=1) |
| | 757 | Group Key: cv.course_version_id, c.course_id, ct.course_translate_id |
| | 758 | -> Incremental Sort (cost=325503.28..497378.56 rows=2400000 width=176) (actual time=936.414..2723.983 rows=2400000 loops=1) |
| | 759 | Sort Key: cv.course_version_id, c.course_id, ct.course_translate_id, e.user_id |
| | 760 | Presorted Key: cv.course_version_id |
| | 761 | Full-sort Groups: 6000 Sort Method: quicksort Average Memory: 35kB Peak Memory: 35kB |
| | 762 | Pre-sorted Groups: 6000 Sort Method: quicksort Average Memory: 89kB Peak Memory: 89kB |
| | 763 | -> Merge Left Join (cost=325479.65..363532.28 rows=2400000 width=176) (actual time=936.128..1192.913 rows=2400000 loops=1) |
| | 764 | Merge Cond: (cv.course_version_id = e.course_version_id) |
| | 765 | -> Sort (cost=2523.56..2568.56 rows=18000 width=91) (actual time=236.097..236.814 rows=18000 loops=1) |
| | 766 | Sort Key: cv.course_version_id |
| | 767 | Sort Method: quicksort Memory: 2692kB |
| | 768 | -> Hash Join (cost=253.07..1251.35 rows=18000 width=91) (actual time=230.258..233.758 rows=18000 loops=1) |
| | 769 | Hash Cond: (c.course_id = cv.course_id) |
| | 770 | -> Hash Join (cost=73.07..853.85 rows=6000 width=86) (actual time=229.654..232.239 rows=6000 loops=1) |
| | 771 | Hash Cond: (ct.language_id = l.id) |
| | 772 | -> Hash Join (cost=72.00..814.78 rows=6000 width=94) (actual time=0.265..2.501 rows=6000 loops=1) |
| | 773 | Hash Cond: (ct.course_id = c.course_id) |
| | 774 | -> Seq Scan on course_translate ct (cost=0.00..727.00 rows=6000 width=77) (actual time=0.023..1.510 rows=6000 loops=1) |
| | 775 | -> Hash (cost=47.00..47.00 rows=2000 width=17) (actual time=0.235..0.235 rows=2000 loops=1) |
| | 776 | Buckets: 2048 Batches: 1 Memory Usage: 112kB |
| | 777 | -> Seq Scan on course c (cost=0.00..47.00 rows=2000 width=17) (actual time=0.009..0.133 rows=2000 loops=1) |
| | 778 | -> Hash (cost=1.03..1.03 rows=3 width=8) (actual time=229.383..229.383 rows=3 loops=1) |
| | 779 | Buckets: 1024 Batches: 1 Memory Usage: 9kB |
| | 780 | -> Seq Scan on language l (cost=0.00..1.03 rows=3 width=8) (actual time=229.370..229.373 rows=3 loops=1) |
| | 781 | -> Hash (cost=105.00..105.00 rows=6000 width=21) (actual time=0.599..0.599 rows=6000 loops=1) |
| | 782 | Buckets: 8192 Batches: 1 Memory Usage: 393kB |
| | 783 | -> Seq Scan on course_version cv (cost=0.00..105.00 rows=6000 width=21) (actual time=0.007..0.300 rows=6000 loops=1) |
| | 784 | -> Materialize (cost=322955.29..326955.29 rows=800000 width=93) (actual time=700.004..814.939 rows=2399998 loops=1) |
| | 785 | -> Sort (cost=322955.29..324955.29 rows=800000 width=93) (actual time=700.002..742.481 rows=800000 loops=1) |
| | 786 | Sort Key: e.course_version_id |
| | 787 | Sort Method: external merge Disk: 63912kB |
| | 788 | -> Merge Left Join (cost=1.70..162483.72 rows=800000 width=93) (actual time=0.069..533.088 rows=800000 loops=1) |
| | 789 | Merge Cond: (e.enrollment_id = ucp.enrollment_id) |
| | 790 | -> Merge Left Join (cost=1.27..77078.27 rows=800000 width=61) (actual time=0.040..257.927 rows=800000 loops=1) |
| | 791 | Merge Cond: (e.enrollment_id = r.enrollment_id) |
| | 792 | -> Merge Left Join (cost=0.85..66772.85 rows=800000 width=49) (actual time=0.027..195.293 rows=800000 loops=1) |
| | 793 | Merge Cond: (e.enrollment_id = p.enrollment_id) |
| | 794 | -> Index Scan using enrollment_pkey on enrollment e (cost=0.42..27318.42 rows=800000 width=32) (actual time=0.014..54.881 rows=800000 loops=1) |
| | 795 | -> Index Scan using uq_payment_enrollment on payment p (cost=0.42..27454.42 rows=800000 width=25) (actual time=0.009..54.968 rows=800000 loops=1) |
| | 796 | -> Index Scan using uq_review_enrollment on review r (cost=0.42..6305.42 rows=160000 width=20) (actual time=0.009..12.316 rows=160000 loops=1) |
| | 797 | -> GroupAggregate (cost=0.42..76660.31 rows=299784 width=40) (actual time=0.026..223.124 rows=320000 loops=1) |
| | 798 | Group Key: ucp.enrollment_id |
| | 799 | -> Index Scan using uq_ucp on user_course_progress ucp (cost=0.42..63464.63 rows=960000 width=9) (actual time=0.014..120.943 rows=960000 loops=1) |
| | 800 | Planning Time: 1.689 ms |
| | 801 | JIT: |
| | 802 | Functions: 58 |
| | 803 | Options: Inlining true, Optimization true, Expressions true, Deforming true |
| | 804 | Timing: Generation 1.016 ms (Deform 0.503 ms), Inlining 31.592 ms, Optimization 104.539 ms, Emission 93.272 ms, Total 230.418 ms |
| | 805 | Execution Time: 2958.273 ms |
| | 806 | }}} |
| | 807 | |
| | 808 | Време: '''2974,0 ms''' (три пуштања: 2960,8 / 3003,1 / 2958,3 ms). |
| | 809 | |
| | 810 | Од планот, `Incremental Sort` трае од 936 ms до 2724 ms, значи околу 1530 ms или приблизно 52% од целото време се троши на едно сортирање. `Merge Left Join` враќа 2.400.000 реда, а функцијата на крај враќа само 18.000. `Materialize` враќа 2.399.998 реда од 800.000 сортирани, што значи дека истите редови се препрочитуваат по еднаш за секој јазик. Сортирањето по `e.course_version_id` се прелева на диск со `external merge Disk: 63912kB`. Најскапиот поединечен scan е `Index Scan using uq_ucp on user_course_progress`, кој чита 960.000 редови за 120,9 ms. И тука `enrollment.course_version_id` нема индекс. |
| | 811 | |
| | 812 | Двете идеи се индекс врз `enrollment.course_version_id`, за да се избегне големото сортирање, и covering индекс врз `user_course_progress`, за CTE-то `lecture_progress` да може да работи преку `Index Only Scan` без пристап до heap-от. |
| | 813 | |
| | 814 | ==== Чекор 1, индекс врз enrollment.course_version_id ==== |
| | 815 | |
| | 816 | {{{ |
| | 817 | CREATE INDEX idx_enrollment_course_version_id\n ON enrollment(course_version_id); |
| | 818 | }}} |
| | 819 | |
| | 820 | {{{ |
| | 821 | Sort (cost=1738846.95..1744846.95 rows=2400000 width=351) (actual time=2943.828..2944.239 rows=18000 loops=1) |
| | 822 | Sort Key: (COALESCE(sum(CASE WHEN (p.payment_status = 'completed'::payment_status) THEN p.amount ELSE '0'::numeric END), '0'::numeric)) DESC, (COALESCE(round(((100.0 * (count(CASE WHEN (e.completion_date IS NOT NULL) THEN e.enrollment_id ELSE NULL::bigint END))::numeric) / (NULLIF(count(e.enrollment_id), 0))::numeric), 2), '0'::numeric)) DESC |
| | 823 | Sort Method: quicksort Memory: 4026kB |
| | 824 | -> GroupAggregate (cost=325500.08..713412.72 rows=2400000 width=351) (actual time=931.922..2934.137 rows=18000 loops=1) |
| | 825 | Group Key: cv.course_version_id, c.course_id, ct.course_translate_id |
| | 826 | -> Incremental Sort (cost=325500.08..497412.72 rows=2400000 width=176) (actual time=931.861..2720.117 rows=2400000 loops=1) |
| | 827 | Sort Key: cv.course_version_id, c.course_id, ct.course_translate_id, e.user_id |
| | 828 | Presorted Key: cv.course_version_id |
| | 829 | Full-sort Groups: 6000 Sort Method: quicksort Average Memory: 35kB Peak Memory: 35kB |
| | 830 | Pre-sorted Groups: 6000 Sort Method: quicksort Average Memory: 89kB Peak Memory: 89kB |
| | 831 | -> Merge Left Join (cost=325476.44..363566.44 rows=2400000 width=176) (actual time=931.564..1184.598 rows=2400000 loops=1) |
| | 832 | Merge Cond: (cv.course_version_id = e.course_version_id) |
| | 833 | -> Sort (cost=2523.56..2568.56 rows=18000 width=91) (actual time=234.588..235.294 rows=18000 loops=1) |
| | 834 | Sort Key: cv.course_version_id |
| | 835 | Sort Method: quicksort Memory: 2692kB |
| | 836 | -> Hash Join (cost=253.07..1251.35 rows=18000 width=91) (actual time=228.752..232.321 rows=18000 loops=1) |
| | 837 | Hash Cond: (c.course_id = cv.course_id) |
| | 838 | -> Hash Join (cost=73.07..853.85 rows=6000 width=86) (actual time=228.156..230.803 rows=6000 loops=1) |
| | 839 | Hash Cond: (ct.language_id = l.id) |
| | 840 | -> Hash Join (cost=72.00..814.78 rows=6000 width=94) (actual time=0.261..2.581 rows=6000 loops=1) |
| | 841 | Hash Cond: (ct.course_id = c.course_id) |
| | 842 | -> Seq Scan on course_translate ct (cost=0.00..727.00 rows=6000 width=77) (actual time=0.023..1.592 rows=6000 loops=1) |
| | 843 | -> Hash (cost=47.00..47.00 rows=2000 width=17) (actual time=0.230..0.231 rows=2000 loops=1) |
| | 844 | Buckets: 2048 Batches: 1 Memory Usage: 112kB |
| | 845 | -> Seq Scan on course c (cost=0.00..47.00 rows=2000 width=17) (actual time=0.008..0.136 rows=2000 loops=1) |
| | 846 | -> Hash (cost=1.03..1.03 rows=3 width=8) (actual time=227.888..227.888 rows=3 loops=1) |
| | 847 | Buckets: 1024 Batches: 1 Memory Usage: 9kB |
| | 848 | -> Seq Scan on language l (cost=0.00..1.03 rows=3 width=8) (actual time=227.875..227.878 rows=3 loops=1) |
| | 849 | -> Hash (cost=105.00..105.00 rows=6000 width=21) (actual time=0.591..0.592 rows=6000 loops=1) |
| | 850 | Buckets: 8192 Batches: 1 Memory Usage: 393kB |
| | 851 | -> Seq Scan on course_version cv (cost=0.00..105.00 rows=6000 width=21) (actual time=0.006..0.295 rows=6000 loops=1) |
| | 852 | -> Materialize (cost=322952.88..326952.88 rows=800000 width=93) (actual time=696.947..810.881 rows=2399998 loops=1) |
| | 853 | -> Sort (cost=322952.88..324952.88 rows=800000 width=93) (actual time=696.944..738.560 rows=800000 loops=1) |
| | 854 | Sort Key: e.course_version_id |
| | 855 | Sort Method: external merge Disk: 63912kB |
| | 856 | -> Merge Left Join (cost=1.70..162481.32 rows=800000 width=93) (actual time=0.071..528.730 rows=800000 loops=1) |
| | 857 | Merge Cond: (e.enrollment_id = ucp.enrollment_id) |
| | 858 | -> Merge Left Join (cost=1.27..77075.86 rows=800000 width=61) (actual time=0.040..258.573 rows=800000 loops=1) |
| | 859 | Merge Cond: (e.enrollment_id = r.enrollment_id) |
| | 860 | -> Merge Left Join (cost=0.85..66770.86 rows=800000 width=49) (actual time=0.028..195.985 rows=800000 loops=1) |
| | 861 | Merge Cond: (e.enrollment_id = p.enrollment_id) |
| | 862 | -> Index Scan using enrollment_pkey on enrollment e (cost=0.42..27318.42 rows=800000 width=32) (actual time=0.015..55.784 rows=800000 loops=1) |
| | 863 | -> Index Scan using uq_payment_enrollment on payment p (cost=0.42..27454.42 rows=800000 width=25) (actual time=0.008..55.376 rows=800000 loops=1) |
| | 864 | -> Index Scan using uq_review_enrollment on review r (cost=0.42..6305.42 rows=160000 width=20) (actual time=0.008..12.441 rows=160000 loops=1) |
| | 865 | -> GroupAggregate (cost=0.42..76660.31 rows=299784 width=40) (actual time=0.028..219.215 rows=320000 loops=1) |
| | 866 | Group Key: ucp.enrollment_id |
| | 867 | -> Index Scan using uq_ucp on user_course_progress ucp (cost=0.42..63464.63 rows=960000 width=9) (actual time=0.013..117.947 rows=960000 loops=1) |
| | 868 | Planning Time: 1.627 ms |
| | 869 | JIT: |
| | 870 | Functions: 58 |
| | 871 | Options: Inlining true, Optimization true, Expressions true, Deforming true |
| | 872 | Timing: Generation 1.100 ms (Deform 0.571 ms), Inlining 31.834 ms, Optimization 103.894 ms, Emission 92.180 ms, Total 229.007 ms |
| | 873 | Execution Time: 2955.216 ms |
| | 874 | }}} |
| | 875 | |
| | 876 | Време: '''2949,2 ms''' (три пуштања: 2941,1 / 2951,3 / 2955,2 ms), односно без ефект (+0,8%, во границите на мерната грешка). |
| | 877 | |
| | 878 | Индексот не е искористен. Планерот останува на веригата merge join-ови по `enrollment_id`, бидејќи `payment`, `review` и `user_course_progress` сите се спојуваат по `enrollment_id` и тие индекси веќе постојат, па дури потоа сортира по `course_version_id`. `external merge Disk: 63912kB` останува. |
| | 879 | |
| | 880 | ==== Чекор 2, covering индекс врз user_course_progress ==== |
| | 881 | |
| | 882 | {{{ |
| | 883 | CREATE INDEX idx_ucp_enrollment_completed\n ON user_course_progress(enrollment_id) INCLUDE (is_completed); |
| | 884 | }}} |
| | 885 | |
| | 886 | {{{ |
| | 887 | Sort (cost=1705431.82..1711431.82 rows=2400000 width=351) (actual time=2850.269..2850.660 rows=18000 loops=1) |
| | 888 | Sort Key: (COALESCE(sum(CASE WHEN (p.payment_status = 'completed'::payment_status) THEN p.amount ELSE '0'::numeric END), '0'::numeric)) DESC, (COALESCE(round(((100.0 * (count(CASE WHEN (e.completion_date IS NOT NULL) THEN e.enrollment_id ELSE NULL::bigint END))::numeric) / (NULLIF(count(e.enrollment_id), 0))::numeric), 2), '0'::numeric)) DESC |
| | 889 | Sort Method: quicksort Memory: 4026kB |
| | 890 | -> GroupAggregate (cost=292084.95..679997.59 rows=2400000 width=351) (actual time=821.100..2841.442 rows=18000 loops=1) |
| | 891 | Group Key: cv.course_version_id, c.course_id, ct.course_translate_id |
| | 892 | -> Incremental Sort (cost=292084.95..463997.59 rows=2400000 width=176) (actual time=821.042..2626.356 rows=2400000 loops=1) |
| | 893 | Sort Key: cv.course_version_id, c.course_id, ct.course_translate_id, e.user_id |
| | 894 | Presorted Key: cv.course_version_id |
| | 895 | Full-sort Groups: 6000 Sort Method: quicksort Average Memory: 35kB Peak Memory: 35kB |
| | 896 | Pre-sorted Groups: 6000 Sort Method: quicksort Average Memory: 89kB Peak Memory: 89kB |
| | 897 | -> Merge Left Join (cost=292061.31..330151.31 rows=2400000 width=176) (actual time=820.735..1077.264 rows=2400000 loops=1) |
| | 898 | Merge Cond: (cv.course_version_id = e.course_version_id) |
| | 899 | -> Sort (cost=2523.56..2568.56 rows=18000 width=91) (actual time=230.300..231.048 rows=18000 loops=1) |
| | 900 | Sort Key: cv.course_version_id |
| | 901 | Sort Method: quicksort Memory: 2692kB |
| | 902 | -> Hash Join (cost=253.07..1251.35 rows=18000 width=91) (actual time=224.575..228.017 rows=18000 loops=1) |
| | 903 | Hash Cond: (c.course_id = cv.course_id) |
| | 904 | -> Hash Join (cost=73.07..853.85 rows=6000 width=86) (actual time=223.945..226.495 rows=6000 loops=1) |
| | 905 | Hash Cond: (ct.language_id = l.id) |
| | 906 | -> Hash Join (cost=72.00..814.78 rows=6000 width=94) (actual time=0.288..2.497 rows=6000 loops=1) |
| | 907 | Hash Cond: (ct.course_id = c.course_id) |
| | 908 | -> Seq Scan on course_translate ct (cost=0.00..727.00 rows=6000 width=77) (actual time=0.027..1.526 rows=6000 loops=1) |
| | 909 | -> Hash (cost=47.00..47.00 rows=2000 width=17) (actual time=0.251..0.251 rows=2000 loops=1) |
| | 910 | Buckets: 2048 Batches: 1 Memory Usage: 112kB |
| | 911 | -> Seq Scan on course c (cost=0.00..47.00 rows=2000 width=17) (actual time=0.011..0.154 rows=2000 loops=1) |
| | 912 | -> Hash (cost=1.03..1.03 rows=3 width=8) (actual time=223.648..223.649 rows=3 loops=1) |
| | 913 | Buckets: 1024 Batches: 1 Memory Usage: 9kB |
| | 914 | -> Seq Scan on language l (cost=0.00..1.03 rows=3 width=8) (actual time=223.634..223.638 rows=3 loops=1) |
| | 915 | -> Hash (cost=105.00..105.00 rows=6000 width=21) (actual time=0.625..0.625 rows=6000 loops=1) |
| | 916 | Buckets: 8192 Batches: 1 Memory Usage: 393kB |
| | 917 | -> Seq Scan on course_version cv (cost=0.00..105.00 rows=6000 width=21) (actual time=0.008..0.305 rows=6000 loops=1) |
| | 918 | -> Materialize (cost=289537.75..293537.75 rows=800000 width=93) (actual time=590.408..704.748 rows=2399998 loops=1) |
| | 919 | -> Sort (cost=289537.75..291537.75 rows=800000 width=93) (actual time=590.404..631.143 rows=800000 loops=1) |
| | 920 | Sort Key: e.course_version_id |
| | 921 | Sort Method: external merge Disk: 63912kB |
| | 922 | -> Merge Left Join (cost=1.70..129066.19 rows=800000 width=93) (actual time=0.074..427.971 rows=800000 loops=1) |
| | 923 | Merge Cond: (e.enrollment_id = ucp.enrollment_id) |
| | 924 | -> Merge Left Join (cost=1.27..77075.86 rows=800000 width=61) (actual time=0.037..255.216 rows=800000 loops=1) |
| | 925 | Merge Cond: (e.enrollment_id = r.enrollment_id) |
| | 926 | -> Merge Left Join (cost=0.85..66770.86 rows=800000 width=49) (actual time=0.025..192.972 rows=800000 loops=1) |
| | 927 | Merge Cond: (e.enrollment_id = p.enrollment_id) |
| | 928 | -> Index Scan using enrollment_pkey on enrollment e (cost=0.42..27318.42 rows=800000 width=32) (actual time=0.013..55.297 rows=800000 loops=1) |
| | 929 | -> Index Scan using uq_payment_enrollment on payment p (cost=0.42..27454.42 rows=800000 width=25) (actual time=0.008..55.497 rows=800000 loops=1) |
| | 930 | -> Index Scan using uq_review_enrollment on review r (cost=0.42..6305.42 rows=160000 width=20) (actual time=0.008..12.381 rows=160000 loops=1) |
| | 931 | -> GroupAggregate (cost=0.42..42782.96 rows=320327 width=40) (actual time=0.034..122.050 rows=320000 loops=1) |
| | 932 | Group Key: ucp.enrollment_id |
| | 933 | -> Index Only Scan using idx_ucp_enrollment_completed on user_course_progress ucp (cost=0.42..29176.42 rows=960000 width=9) (actual time=0.028..46.170 rows=960000 loops=1) |
| | 934 | Heap Fetches: 0 |
| | 935 | Planning Time: 1.702 ms |
| | 936 | JIT: |
| | 937 | Functions: 56 |
| | 938 | Options: Inlining true, Optimization true, Expressions true, Deforming true |
| | 939 | Timing: Generation 1.158 ms (Deform 0.626 ms), Inlining 32.279 ms, Optimization 102.678 ms, Emission 88.707 ms, Total 224.822 ms |
| | 940 | Execution Time: 2862.106 ms |
| | 941 | }}} |
| | 942 | |
| | 943 | Време: '''2846,2 ms''' (три пуштања: 2832,2 / 2844,2 / 2862,1 ms), односно маргинално забрзување од 4,3%. |
| | 944 | |
| | 945 | Ова е најдобриот резултат од сите индекси во оваа анализа. Причината е видлива во планот, скапиот scan врз 960.000 редови се промени од: |
| | 946 | |
| | 947 | {{{ |
| | 948 | Index Scan using uq_ucp on user_course_progress ucp\n (actual time=0.014..120.943 rows=960000 loops=1) |
| | 949 | }}} |
| | 950 | |
| | 951 | во: |
| | 952 | |
| | 953 | {{{ |
| | 954 | Index Only Scan using idx_ucp_enrollment_completed on user_course_progress ucp\n (actual time=0.028..46.170 rows=960000 loops=1) |
| | 955 | }}} |
| | 956 | |
| | 957 | Тој јазол стана '''2,6 пати побрз''', од 120,9 ms на 46,2 ms, бидејќи `is_completed` сега се чита директно од индексот и воопшто не се пристапува до heap-от. |
| | 958 | |
| | 959 | ==== Проверка со work_mem ==== |
| | 960 | |
| | 961 | Со `work_mem = 256 MB` сортирањето од 63 MB престанува да се прелева на диск. |
| | 962 | |
| | 963 | {{{ |
| | 964 | Sort Method: quicksort Memory: 95827kB\nExecution Time: 3117.918 ms |
| | 965 | }}} |
| | 966 | |
| | 967 | Резултатот е побавен, 3117,9 ms наспроти 2974,0 ms, односно околу 5% позагубено. Причината е што `quicksort` врз околу 96 MB во меморија е поскап од `external merge sort`, кој е оптимизиран за големи количини и чии привремени датотеки и онака се во page cache. Ова е добар потсетник дека поголем `work_mem` не значи автоматски побрзо. |
| | 968 | |
| | 969 | ==== Колку чини враќањето на сите јазици ==== |
| | 970 | |
| | 971 | Функцијата намерно враќа резултат за сите три јазика. Вреди сепак да се измери колку чини таа одлука. Со додаден филтер за еден јазик: |
| | 972 | |
| | 973 | {{{ |
| | 974 | JOIN language l ON l.id = ct.language_id AND l.value = 'en' |
| | 975 | }}} |
| | 976 | |
| | 977 | и со истите два индекси од чекор 1 и 2, времето паѓа на '''1666,2 ms''' (три пуштања: 1663,9 / 1669,7 / 1665,1 ms), а резултатот има 6.000 наместо 18.000 реда. |
| | 978 | |
| | 979 | || '''Варијанта''' || '''Редови''' || '''Време''' || |
| | 980 | || сите јазици, тековна || 18.000 || 2846,2 ms || |
| | 981 | || само `en` || 6.000 || 1666,2 ms || |
| | 982 | |
| | 983 | Тоа е '''1,71 пати побрзо''', односно 41% помалку време, што е многукратно повеќе од сето што донесоа индексите заедно. |
| | 984 | |
| | 985 | Ова не е предлог да се смени функцијата, бидејќи враќањето на сите јазици е намерна одлука и филтрирањето би го променило резултатот. Наведено е само за да се види каде реално оди времето, бидејќи три пати повеќе редови значат приблизно двојно повеќе време. Ако некогаш се појави потреба од извештај само за еден јазик, најефтино е јазикот да биде параметар на функцијата, наместо да се филтрира дополнително врз готовиот резултат. |
| | 986 | |
| | 987 | ==== Заклучок ==== |
| | 988 | |
| | 989 | Индексот помогна, но само 4,3%, и тоа е поучно само по себе. Covering индексот врз `user_course_progress` направи тој дел од планот да биде 2,6 пати побрз, но вкупното query забрза само за 4,3%. Ова е директна илустрација на Амдаловиот закон, бидејќи ако забрзаш дел што зафаќа само неколку проценти од времето, крајната добивка не може да надмине тие неколку проценти. |
| | 990 | |
| | 991 | Заштедените околу 75 ms се реални, но останатите околу 2850 ms се трошат на `Incremental Sort` врз 2,4 милиони редови, кој е најскапиот дел со околу 1530 ms, на веригата merge join-ови врз 800.000 редови, на сортирањето по `course_version_id` што се прелева на диск, и на `GroupAggregate` со `COUNT(DISTINCT e.user_id)`. |
| | 992 | |
| | 993 | Индексот врз `course_version_id` од чекор 1 не помогна воопшто, затоа што планерот има подобра алтернатива. Сите четири табели во веригата, `enrollment`, `payment`, `review` и `user_course_progress`, се спојуваат по `enrollment_id`, а по тој столб веќе постојат unique индекси. Планерот затоа чита сè подредено по `enrollment_id` со merge join-ови, што е поевтино од тоа да влезе преку `course_version_id` и потоа да прави случајни пристапи. Поуката е дека индекс се користи само ако планерот процени дека е поевтин од алтернативата, па создавањето индекс не значи дека тој ќе биде употребен. |
| | 994 | |
| | 995 | Вториот заклучок, од мерењето со филтер по јазик, е дека бројот на редови што влегуваат во агрегацијата е далеку поважен од индексите. Трите јазика ја прават функцијата 1,71 пати побавна, додека индексите донесоа 4,3%. |
| | 996 | |
| | 997 | === dashboard_expert_performance() === |
| | 998 | |
| | 999 | Збирна изведба по експерт. Резултатот има 50.000 реда, по еден за секој експерт. |
| | 1000 | |
| | 1001 | ==== Дефиниција ==== |
| | 1002 | |
| | 1003 | {{{ |
| | 1004 | -- Expert performance summary |
| | 1005 | CREATE OR REPLACE FUNCTION dashboard_expert_performance() |
| | 1006 | RETURNS TABLE ( |
| | 1007 | expert_id INTEGER, |
| | 1008 | expert_name TEXT, |
| | 1009 | courses_created BIGINT, |
| | 1010 | total_enrollments BIGINT, |
| | 1011 | paid_enrollments BIGINT, |
| | 1012 | trial_enrollments BIGINT, |
| | 1013 | total_revenue NUMERIC, |
| | 1014 | avg_rating NUMERIC, |
| | 1015 | total_reviews BIGINT |
| | 1016 | ) AS $$ |
| | 1017 | #variable_conflict use_column |
| | 1018 | BEGIN |
| | 1019 | RETURN QUERY |
| | 1020 | SELECT |
| | 1021 | ex.expert_id::INTEGER AS expert_id, |
| | 1022 | a.name::TEXT AS expert_name, |
| | 1023 | COUNT(DISTINCT c.course_id)::BIGINT AS courses_created, |
| | 1024 | COUNT(DISTINCT e.enrollment_id)::BIGINT AS total_enrollments, |
| | 1025 | COUNT(DISTINCT CASE WHEN p.payment_status = 'completed' THEN e.enrollment_id END)::BIGINT AS paid_enrollments, |
| | 1026 | COUNT(DISTINCT CASE WHEN p.payment_id IS NULL THEN e.enrollment_id END)::BIGINT AS trial_enrollments, |
| | 1027 | COALESCE(SUM(CASE WHEN p.payment_status = 'completed' THEN p.amount ELSE 0 END), 0)::NUMERIC AS total_revenue, |
| | 1028 | COALESCE(AVG(r.rating), 0)::NUMERIC AS avg_rating, |
| | 1029 | COUNT(r.review_id)::BIGINT AS total_reviews |
| | 1030 | FROM expert ex |
| | 1031 | JOIN account a ON a.id = ex.account_id -- expert name lives on account |
| | 1032 | LEFT JOIN expert_course ec ON ex.expert_id = ec.expert_id -- experts with no courses should be included |
| | 1033 | LEFT JOIN course c ON ec.course_id = c.course_id -- experts with no courses should be included |
| | 1034 | LEFT JOIN course_version cv ON c.course_id = cv.course_id -- left join so experts without courses are not dropped |
| | 1035 | LEFT JOIN enrollment e ON cv.course_version_id = e.course_version_id -- left join to include courses with zero enrollments |
| | 1036 | LEFT JOIN payment p ON e.enrollment_id = p.enrollment_id -- left join to include enrollments without payments |
| | 1037 | LEFT JOIN review r ON e.enrollment_id = r.enrollment_id -- left join to include enrollments without reviews |
| | 1038 | GROUP BY ex.expert_id, a.id |
| | 1039 | ORDER BY total_revenue DESC NULLS LAST; |
| | 1040 | END; |
| | 1041 | $$ LANGUAGE plpgsql; |
| | 1042 | }}} |
| | 1043 | |
| | 1044 | ==== Почетна состојба ==== |
| | 1045 | |
| | 1046 | {{{ |
| | 1047 | Sort (cost=9416816.80..9466816.80 rows=20000000 width=156) (actual time=1620.638..1623.502 rows=50000 loops=1) |
| | 1048 | Sort Key: (COALESCE(sum(CASE WHEN (p.payment_status = 'completed'::payment_status) THEN p.amount ELSE '0'::numeric END), '0'::numeric)) DESC NULLS LAST |
| | 1049 | Sort Method: external merge Disk: 5216kB |
| | 1050 | -> GroupAggregate (cost=436066.06..2274667.63 rows=20000000 width=156) (actual time=841.856..1610.629 rows=50000 loops=1) |
| | 1051 | Group Key: ex.expert_id, a.id |
| | 1052 | -> Incremental Sort (cost=436066.06..1374667.63 rows=20000000 width=82) (actual time=841.225..1328.382 rows=1913735 loops=1) |
| | 1053 | Sort Key: ex.expert_id, a.id, c.course_id |
| | 1054 | Presorted Key: ex.expert_id, a.id |
| | 1055 | Full-sort Groups: 4073 Sort Method: quicksort Average Memory: 31kB Peak Memory: 31kB |
| | 1056 | Pre-sorted Groups: 4986 Sort Method: quicksort Average Memory: 217kB Peak Memory: 218kB |
| | 1057 | -> Merge Left Join (cost=436066.05..474667.63 rows=20000000 width=82) (actual time=840.824..1123.142 rows=1913735 loops=1) |
| | 1058 | Merge Cond: (ex.expert_id = ec.expert_id) |
| | 1059 | -> Gather Merge (cost=13663.64..19486.97 rows=50000 width=37) (actual time=179.596..182.605 rows=50000 loops=1) |
| | 1060 | Workers Planned: 2 |
| | 1061 | Workers Launched: 2 |
| | 1062 | -> Sort (cost=12663.62..12715.70 rows=20833 width=37) (actual time=118.464..118.848 rows=16667 loops=3) |
| | 1063 | Sort Key: ex.expert_id, a.id |
| | 1064 | Sort Method: quicksort Memory: 25kB |
| | 1065 | Worker 0: Sort Method: quicksort Memory: 2117kB |
| | 1066 | Worker 1: Sort Method: quicksort Memory: 2154kB |
| | 1067 | -> Hash Join (cost=1396.00..11169.21 rows=20833 width=37) (actual time=114.640..117.284 rows=16667 loops=3) |
| | 1068 | Hash Cond: (a.id = ex.account_id) |
| | 1069 | -> Parallel Seq Scan on account a (cost=0.00..9226.33 rows=208333 width=29) (actual time=0.015..8.180 rows=166667 loops=3) |
| | 1070 | -> Hash (cost=771.00..771.00 rows=50000 width=16) (actual time=98.396..98.396 rows=50000 loops=3) |
| | 1071 | Buckets: 65536 Batches: 1 Memory Usage: 2856kB |
| | 1072 | -> Seq Scan on expert ex (cost=0.00..771.00 rows=50000 width=16) (actual time=0.006..1.642 rows=50000 loops=3) |
| | 1073 | -> Materialize (cost=422393.66..431725.66 rows=1866400 width=53) (actual time=661.199..836.447 rows=1866401 loops=1) |
| | 1074 | -> Sort (cost=422393.66..427059.66 rows=1866400 width=53) (actual time=661.196..742.643 rows=1866401 loops=1) |
| | 1075 | Sort Key: ec.expert_id |
| | 1076 | Sort Method: external merge Disk: 107416kB |
| | 1077 | -> Hash Right Join (cost=666.96..100402.06 rows=1866400 width=53) (actual time=3.098..377.129 rows=1866401 loops=1) |
| | 1078 | Hash Cond: (e.course_version_id = cv.course_version_id) |
| | 1079 | -> Merge Left Join (cost=1.75..77072.85 rows=800000 width=45) (actual time=0.040..266.632 rows=800000 loops=1) |
| | 1080 | Merge Cond: (e.enrollment_id = r.enrollment_id) |
| | 1081 | -> Merge Left Join (cost=1.33..66768.43 rows=800000 width=33) (actual time=0.028..203.477 rows=800000 loops=1) |
| | 1082 | Merge Cond: (e.enrollment_id = p.enrollment_id) |
| | 1083 | -> Index Scan using enrollment_pkey on enrollment e (cost=0.42..27318.42 rows=800000 width=16) (actual time=0.014..59.040 rows=800000 loops=1) |
| | 1084 | -> Index Scan using uq_payment_enrollment on payment p (cost=0.42..27454.42 rows=800000 width=25) (actual time=0.009..58.879 rows=800000 loops=1) |
| | 1085 | -> Index Scan using uq_review_enrollment on review r (cost=0.42..6305.42 rows=160000 width=20) (actual time=0.008..13.251 rows=160000 loops=1) |
| | 1086 | -> Hash (cost=490.24..490.24 rows=13998 width=24) (actual time=3.017..3.019 rows=13998 loops=1) |
| | 1087 | Buckets: 16384 Batches: 1 Memory Usage: 894kB |
| | 1088 | -> Hash Right Join (cost=215.26..490.24 rows=13998 width=24) (actual time=1.085..2.207 rows=13998 loops=1) |
| | 1089 | Hash Cond: (cv.course_id = c.course_id) |
| | 1090 | -> Seq Scan on course_version cv (cost=0.00..105.00 rows=6000 width=16) (actual time=0.008..0.297 rows=6000 loops=1) |
| | 1091 | -> Hash (cost=156.93..156.93 rows=4666 width=16) (actual time=1.057..1.058 rows=4666 loops=1) |
| | 1092 | Buckets: 8192 Batches: 1 Memory Usage: 283kB |
| | 1093 | -> Hash Left Join (cost=72.00..156.93 rows=4666 width=16) (actual time=0.259..0.779 rows=4666 loops=1) |
| | 1094 | Hash Cond: (ec.course_id = c.course_id) |
| | 1095 | -> Seq Scan on expert_course ec (cost=0.00..72.66 rows=4666 width=16) (actual time=0.010..0.199 rows=4666 loops=1) |
| | 1096 | -> Hash (cost=47.00..47.00 rows=2000 width=8) (actual time=0.244..0.244 rows=2000 loops=1) |
| | 1097 | Buckets: 2048 Batches: 1 Memory Usage: 95kB |
| | 1098 | -> Seq Scan on course c (cost=0.00..47.00 rows=2000 width=8) (actual time=0.013..0.139 rows=2000 loops=1) |
| | 1099 | Planning Time: 1.235 ms |
| | 1100 | JIT: |
| | 1101 | Functions: 78 |
| | 1102 | Options: Inlining true, Optimization true, Expressions true, Deforming true |
| | 1103 | Timing: Generation 1.617 ms (Deform 0.862 ms), Inlining 102.392 ms, Optimization 98.491 ms, Emission 82.286 ms, Total 284.785 ms |
| | 1104 | Execution Time: 1637.668 ms |
| | 1105 | }}} |
| | 1106 | |
| | 1107 | Време: '''1677,6 ms''' (три пуштања: 1722,1 / 1673,0 / 1637,7 ms). |
| | 1108 | |
| | 1109 | Од планот, сортирањето по `ec.expert_id` се прелева на диск со `external merge Disk: 107416kB`, што е 107 MB и најголемото прелевање во целата анализа, и трае околу 661 ms. `Merge Left Join` враќа 1.913.735 реда, иако на крај се враќаат само 50.000. `Parallel Seq Scan on account` чита сите 500.000 сметки за да ги најде 50.000-те што припаѓаат на експерти. Освен тоа, `expert_course.course_id` нема индекс, бидејќи `pk_expert_course` е `(expert_id, course_id)` и покрива само `expert_id`. |
| | 1110 | |
| | 1111 | Идеите се индекси врз непокриените foreign key-ови, `enrollment.course_version_id` и `expert_course.course_id`, како и covering индекси врз `payment` и `review` за `Index Only Scan` во веригата join-ови. |
| | 1112 | |
| | 1113 | ==== Чекор 1, индекси врз непокриените foreign key-ови ==== |
| | 1114 | |
| | 1115 | {{{ |
| | 1116 | CREATE INDEX idx_enrollment_course_version_id ON enrollment(course_version_id);\nCREATE INDEX idx_expert_course_course_id ON expert_course(course_id); |
| | 1117 | }}} |
| | 1118 | |
| | 1119 | {{{ |
| | 1120 | Sort (cost=9416821.36..9466821.36 rows=20000000 width=156) (actual time=1605.269..1608.025 rows=50000 loops=1) |
| | 1121 | Sort Key: (COALESCE(sum(CASE WHEN (p.payment_status = 'completed'::payment_status) THEN p.amount ELSE '0'::numeric END), '0'::numeric)) DESC NULLS LAST |
| | 1122 | Sort Method: external merge Disk: 5216kB |
| | 1123 | -> GroupAggregate (cost=436070.63..2274672.19 rows=20000000 width=156) (actual time=830.945..1594.989 rows=50000 loops=1) |
| | 1124 | Group Key: ex.expert_id, a.id |
| | 1125 | -> Incremental Sort (cost=436070.63..1374672.19 rows=20000000 width=82) (actual time=830.323..1315.718 rows=1913735 loops=1) |
| | 1126 | Sort Key: ex.expert_id, a.id, c.course_id |
| | 1127 | Presorted Key: ex.expert_id, a.id |
| | 1128 | Full-sort Groups: 4073 Sort Method: quicksort Average Memory: 31kB Peak Memory: 31kB |
| | 1129 | Pre-sorted Groups: 4986 Sort Method: quicksort Average Memory: 217kB Peak Memory: 218kB |
| | 1130 | -> Merge Left Join (cost=436070.61..474672.19 rows=20000000 width=82) (actual time=829.891..1113.685 rows=1913735 loops=1) |
| | 1131 | Merge Cond: (ex.expert_id = ec.expert_id) |
| | 1132 | -> Gather Merge (cost=13663.64..19486.97 rows=50000 width=37) (actual time=174.600..177.658 rows=50000 loops=1) |
| | 1133 | Workers Planned: 2 |
| | 1134 | Workers Launched: 2 |
| | 1135 | -> Sort (cost=12663.62..12715.70 rows=20833 width=37) (actual time=114.172..114.547 rows=16667 loops=3) |
| | 1136 | Sort Key: ex.expert_id, a.id |
| | 1137 | Sort Method: quicksort Memory: 25kB |
| | 1138 | Worker 0: Sort Method: quicksort Memory: 2150kB |
| | 1139 | Worker 1: Sort Method: quicksort Memory: 2121kB |
| | 1140 | -> Hash Join (cost=1396.00..11169.21 rows=20833 width=37) (actual time=111.012..113.206 rows=16667 loops=3) |
| | 1141 | Hash Cond: (a.id = ex.account_id) |
| | 1142 | -> Parallel Seq Scan on account a (cost=0.00..9226.33 rows=208333 width=29) (actual time=0.017..8.058 rows=166667 loops=3) |
| | 1143 | -> Hash (cost=771.00..771.00 rows=50000 width=16) (actual time=94.795..94.795 rows=50000 loops=3) |
| | 1144 | Buckets: 65536 Batches: 1 Memory Usage: 2856kB |
| | 1145 | -> Seq Scan on expert ex (cost=0.00..771.00 rows=50000 width=16) (actual time=0.006..1.703 rows=50000 loops=3) |
| | 1146 | -> Materialize (cost=422398.23..431730.23 rows=1866400 width=53) (actual time=655.263..831.308 rows=1866401 loops=1) |
| | 1147 | -> Sort (cost=422398.23..427064.23 rows=1866400 width=53) (actual time=655.260..738.876 rows=1866401 loops=1) |
| | 1148 | Sort Key: ec.expert_id |
| | 1149 | Sort Method: external merge Disk: 107416kB |
| | 1150 | -> Hash Right Join (cost=667.77..100406.62 rows=1866400 width=53) (actual time=2.857..370.398 rows=1866401 loops=1) |
| | 1151 | Hash Cond: (e.course_version_id = cv.course_version_id) |
| | 1152 | -> Merge Left Join (cost=2.56..77077.41 rows=800000 width=45) (actual time=0.039..261.857 rows=800000 loops=1) |
| | 1153 | Merge Cond: (e.enrollment_id = r.enrollment_id) |
| | 1154 | -> Merge Left Join (cost=2.14..66772.11 rows=800000 width=33) (actual time=0.028..199.757 rows=800000 loops=1) |
| | 1155 | Merge Cond: (e.enrollment_id = p.enrollment_id) |
| | 1156 | -> Index Scan using enrollment_pkey on enrollment e (cost=0.42..27318.42 rows=800000 width=16) (actual time=0.015..57.657 rows=800000 loops=1) |
| | 1157 | -> Index Scan using uq_payment_enrollment on payment p (cost=0.42..27454.42 rows=800000 width=25) (actual time=0.009..57.687 rows=800000 loops=1) |
| | 1158 | -> Index Scan using uq_review_enrollment on review r (cost=0.42..6305.42 rows=160000 width=20) (actual time=0.008..12.992 rows=160000 loops=1) |
| | 1159 | -> Hash (cost=490.24..490.24 rows=13998 width=24) (actual time=2.793..2.795 rows=13998 loops=1) |
| | 1160 | Buckets: 16384 Batches: 1 Memory Usage: 894kB |
| | 1161 | -> Hash Right Join (cost=215.26..490.24 rows=13998 width=24) (actual time=0.996..2.052 rows=13998 loops=1) |
| | 1162 | Hash Cond: (cv.course_id = c.course_id) |
| | 1163 | -> Seq Scan on course_version cv (cost=0.00..105.00 rows=6000 width=16) (actual time=0.007..0.296 rows=6000 loops=1) |
| | 1164 | -> Hash (cost=156.93..156.93 rows=4666 width=16) (actual time=0.976..0.977 rows=4666 loops=1) |
| | 1165 | Buckets: 8192 Batches: 1 Memory Usage: 283kB |
| | 1166 | -> Hash Left Join (cost=72.00..156.93 rows=4666 width=16) (actual time=0.232..0.744 rows=4666 loops=1) |
| | 1167 | Hash Cond: (ec.course_id = c.course_id) |
| | 1168 | -> Seq Scan on expert_course ec (cost=0.00..72.66 rows=4666 width=16) (actual time=0.009..0.193 rows=4666 loops=1) |
| | 1169 | -> Hash (cost=47.00..47.00 rows=2000 width=8) (actual time=0.219..0.220 rows=2000 loops=1) |
| | 1170 | Buckets: 2048 Batches: 1 Memory Usage: 95kB |
| | 1171 | -> Seq Scan on course c (cost=0.00..47.00 rows=2000 width=8) (actual time=0.009..0.132 rows=2000 loops=1) |
| | 1172 | Planning Time: 1.373 ms |
| | 1173 | JIT: |
| | 1174 | Functions: 78 |
| | 1175 | Options: Inlining true, Optimization true, Expressions true, Deforming true |
| | 1176 | Timing: Generation 1.284 ms (Deform 0.701 ms), Inlining 94.140 ms, Optimization 97.362 ms, Emission 81.477 ms, Total 274.263 ms |
| | 1177 | Execution Time: 1621.652 ms |
| | 1178 | }}} |
| | 1179 | |
| | 1180 | Време: '''1669,3 ms''' (три пуштања: 1666,7 / 1719,6 / 1621,7 ms), односно без ефект (+0,5%, во границите на мерната грешка). |
| | 1181 | |
| | 1182 | `external merge Disk: 107416kB` останува непроменето. |
| | 1183 | |
| | 1184 | ==== Чекор 2, covering индекси врз payment и review ==== |
| | 1185 | |
| | 1186 | {{{ |
| | 1187 | CREATE INDEX idx_payment_cov_all\n ON payment(enrollment_id) INCLUDE (amount, payment_status, payment_id);\n\nCREATE INDEX idx_review_cov\n ON review(enrollment_id) INCLUDE (rating, review_id); |
| | 1188 | }}} |
| | 1189 | |
| | 1190 | {{{ |
| | 1191 | Sort (cost=9416093.11..9466093.11 rows=20000000 width=156) (actual time=1597.646..1600.382 rows=50000 loops=1) |
| | 1192 | Sort Key: (COALESCE(sum(CASE WHEN (p.payment_status = 'completed'::payment_status) THEN p.amount ELSE '0'::numeric END), '0'::numeric)) DESC NULLS LAST |
| | 1193 | Sort Method: external merge Disk: 5216kB |
| | 1194 | -> GroupAggregate (cost=435342.38..2273943.94 rows=20000000 width=156) (actual time=822.516..1588.476 rows=50000 loops=1) |
| | 1195 | Group Key: ex.expert_id, a.id |
| | 1196 | -> Incremental Sort (cost=435342.38..1373943.94 rows=20000000 width=82) (actual time=821.886..1309.364 rows=1913735 loops=1) |
| | 1197 | Sort Key: ex.expert_id, a.id, c.course_id |
| | 1198 | Presorted Key: ex.expert_id, a.id |
| | 1199 | Full-sort Groups: 4073 Sort Method: quicksort Average Memory: 31kB Peak Memory: 31kB |
| | 1200 | Pre-sorted Groups: 4986 Sort Method: quicksort Average Memory: 217kB Peak Memory: 218kB |
| | 1201 | -> Merge Left Join (cost=435342.36..473943.94 rows=20000000 width=82) (actual time=821.466..1102.978 rows=1913735 loops=1) |
| | 1202 | Merge Cond: (ex.expert_id = ec.expert_id) |
| | 1203 | -> Gather Merge (cost=13663.64..19486.97 rows=50000 width=37) (actual time=166.177..169.143 rows=50000 loops=1) |
| | 1204 | Workers Planned: 2 |
| | 1205 | Workers Launched: 2 |
| | 1206 | -> Sort (cost=12663.62..12715.70 rows=20833 width=37) (actual time=112.352..112.718 rows=16667 loops=3) |
| | 1207 | Sort Key: ex.expert_id, a.id |
| | 1208 | Sort Method: quicksort Memory: 25kB |
| | 1209 | Worker 0: Sort Method: quicksort Memory: 2133kB |
| | 1210 | Worker 1: Sort Method: quicksort Memory: 2138kB |
| | 1211 | -> Hash Join (cost=1396.00..11169.21 rows=20833 width=37) (actual time=109.228..111.420 rows=16667 loops=3) |
| | 1212 | Hash Cond: (a.id = ex.account_id) |
| | 1213 | -> Parallel Seq Scan on account a (cost=0.00..9226.33 rows=208333 width=29) (actual time=0.017..8.065 rows=166667 loops=3) |
| | 1214 | -> Hash (cost=771.00..771.00 rows=50000 width=16) (actual time=93.237..93.237 rows=50000 loops=3) |
| | 1215 | Buckets: 65536 Batches: 1 Memory Usage: 2856kB |
| | 1216 | -> Seq Scan on expert ex (cost=0.00..771.00 rows=50000 width=16) (actual time=0.006..1.664 rows=50000 loops=3) |
| | 1217 | -> Materialize (cost=421669.98..431001.98 rows=1866400 width=53) (actual time=655.259..830.777 rows=1866401 loops=1) |
| | 1218 | -> Sort (cost=421669.98..426335.98 rows=1866400 width=53) (actual time=655.256..738.073 rows=1866401 loops=1) |
| | 1219 | Sort Key: ec.expert_id |
| | 1220 | Sort Method: external merge Disk: 107416kB |
| | 1221 | -> Hash Right Join (cost=667.96..99678.37 rows=1866400 width=53) (actual time=3.019..372.200 rows=1866401 loops=1) |
| | 1222 | Hash Cond: (e.course_version_id = cv.course_version_id) |
| | 1223 | -> Merge Left Join (cost=2.75..76349.16 rows=800000 width=45) (actual time=0.117..259.226 rows=800000 loops=1) |
| | 1224 | Merge Cond: (e.enrollment_id = r.enrollment_id) |
| | 1225 | -> Merge Left Join (cost=2.10..66772.85 rows=800000 width=33) (actual time=0.103..204.048 rows=800000 loops=1) |
| | 1226 | Merge Cond: (e.enrollment_id = p.enrollment_id) |
| | 1227 | -> Index Scan using enrollment_pkey on enrollment e (cost=0.42..27318.42 rows=800000 width=16) (actual time=0.017..60.059 rows=800000 loops=1) |
| | 1228 | -> Index Scan using uq_payment_enrollment on payment p (cost=0.42..27454.42 rows=800000 width=25) (actual time=0.073..60.156 rows=800000 loops=1) |
| | 1229 | -> Index Only Scan using idx_review_cov on review r (cost=0.42..5576.42 rows=160000 width=20) (actual time=0.009..8.833 rows=160000 loops=1) |
| | 1230 | Heap Fetches: 0 |
| | 1231 | -> Hash (cost=490.24..490.24 rows=13998 width=24) (actual time=2.872..2.874 rows=13998 loops=1) |
| | 1232 | Buckets: 16384 Batches: 1 Memory Usage: 894kB |
| | 1233 | -> Hash Right Join (cost=215.26..490.24 rows=13998 width=24) (actual time=0.992..2.084 rows=13998 loops=1) |
| | 1234 | Hash Cond: (cv.course_id = c.course_id) |
| | 1235 | -> Seq Scan on course_version cv (cost=0.00..105.00 rows=6000 width=16) (actual time=0.009..0.314 rows=6000 loops=1) |
| | 1236 | -> Hash (cost=156.93..156.93 rows=4666 width=16) (actual time=0.969..0.970 rows=4666 loops=1) |
| | 1237 | Buckets: 8192 Batches: 1 Memory Usage: 283kB |
| | 1238 | -> Hash Left Join (cost=72.00..156.93 rows=4666 width=16) (actual time=0.240..0.742 rows=4666 loops=1) |
| | 1239 | Hash Cond: (ec.course_id = c.course_id) |
| | 1240 | -> Seq Scan on expert_course ec (cost=0.00..72.66 rows=4666 width=16) (actual time=0.015..0.202 rows=4666 loops=1) |
| | 1241 | -> Hash (cost=47.00..47.00 rows=2000 width=8) (actual time=0.220..0.220 rows=2000 loops=1) |
| | 1242 | Buckets: 2048 Batches: 1 Memory Usage: 95kB |
| | 1243 | -> Seq Scan on course c (cost=0.00..47.00 rows=2000 width=8) (actual time=0.012..0.133 rows=2000 loops=1) |
| | 1244 | Planning Time: 1.353 ms |
| | 1245 | JIT: |
| | 1246 | Functions: 76 |
| | 1247 | Options: Inlining true, Optimization true, Expressions true, Deforming true |
| | 1248 | Timing: Generation 1.455 ms (Deform 0.796 ms), Inlining 95.801 ms, Optimization 92.443 ms, Emission 80.145 ms, Total 269.843 ms |
| | 1249 | Execution Time: 1613.605 ms |
| | 1250 | }}} |
| | 1251 | |
| | 1252 | Време: '''1605,8 ms''' (три пуштања: 1606,0 / 1597,8 / 1613,6 ms), односно маргинално забрзување од 4,3%. |
| | 1253 | |
| | 1254 | `review` премина на `Index Only Scan`, од 13,3 ms на 8,8 ms на тој јазол, но `payment` остана на обичен `Index Scan`, бидејќи планерот процени дека covering индексот од 38 MB не се исплати наспроти веќе постоечкиот `uq_payment_enrollment`. |
| | 1255 | |
| | 1256 | ==== Заклучок ==== |
| | 1257 | |
| | 1258 | Индексите донесоа 4,3%, бидејќи проблемот не е во пристапот до податоците. Најскапиот дел од планот е сортирањето на 1,86 милиони редови по `ec.expert_id`, со 107 MB прелевање на диск. Тоа сортирање се прави врз резултат од join, не врз базна табела, а индекс може да даде подреденост само на базна табела. Затоа не постои индекс што би го отстранил. |
| | 1259 | |
| | 1260 | Вистинскиот проблем е структурен. Query-то прави join од `expert` сè до `enrollment`, `payment` и `review`, при што бројот на редови расте на речиси 2 милиони, а потоа со `COUNT(DISTINCT ...)` се собира назад на 50.000. Со други зборови, се обработуваат околу 38 пати повеќе редови отколку што има во резултатот. |
| | 1261 | |
| | 1262 | Индексите не можат да го поправат тоа. Она што би можело е пред-агрегирање на `enrollment` и `payment` по `course_version_id` во подquery пред join-от со `expert`, така што множењето на редови никогаш не се случува, но тоа е надвор од опсегот на оваа анализа. |
| | 1263 | |
| | 1264 | === Општ заклучок === |
| | 1265 | |
| | 1266 | Збирни резултати: |
| | 1267 | |
| | 1268 | || '''Функција''' || '''Почетна''' || '''Со индекси''' || '''Добивка''' || |
| | 1269 | || `dashboard_monthly_totals()` || 153,8 ms || 161,7 ms || -5,1% || |
| | 1270 | || `dashboard_monthly_courses()` || 3434,4 ms || 3360,9 ms || +2,1% || |
| | 1271 | || `dashboard_course_performance()` || 2974,0 ms || 2846,2 ms || +4,3% || |
| | 1272 | || `dashboard_expert_performance()` || 1677,6 ms || 1605,8 ms || +4,3% || |
| | 1273 | |
| | 1274 | Добивката се движи од −5% до +4%, што за ниту една од четирите функции не претставува значајно подобрување. Ова не е неуспех на експериментот, туку очекуван резултат, и причините се четири. |
| | 1275 | |
| | 1276 | Првата е што индексот служи за да се прескокнат редови, а овие query-а не прескокнуваат ништо. Сите четири функции се извештаи што агрегираат врз целата база и немаат `WHERE` услов што ограничува на период, курс или корисник. Кога на query-то му требаат сите 800.000 редови, `Seq Scan` е оптималниот начин да се прочитаат, бидејќи чита цели блокови по редослед без индиректност. Кај `dashboard_monthly_totals()` присилното користење на индекси е дури 1,8 пати побавно од секвенцијалното читање. Индексите помагаат драматично доколку функциите примаа параметри, што не е претпоставка туку измерено: со филтер по период, 70.152 од 800.000 реда, и индекс врз `enrollment(purchase_date)`, функција 1 станува 4,0 пати побрза, од 153,8 ms на 38,1 ms, и планерот го користи индексот што претходно го игнорираше. |
| | 1277 | |
| | 1278 | Втората е што најскапите операции се сортирања врз резултат од join, а не пристап до податоци. Кај функциите 2, 3 и 4 доминантниот трошок е `Sort` или `Incremental Sort`, до 76% од времето кај `dashboard_monthly_courses()`. Тие сортирања се прават врз меѓурезултат од join, чиј клуч содржи столбови од повеќе табели и пресметани изрази. Индексот дава подреденост само во рамките на една базна табела, па таквото сортирање принципиелно не може да се елиминира со индекс. |
| | 1279 | |
| | 1280 | Третата е што најкорисните индекси веќе постојат. Сите join-ови во овие функции одат по `enrollment_id`, `course_id` и `course_version_id`, а тие се primary key-ови или unique constraint-и, што значи дека индексите веќе се создадени автоматски. Токму затоа новите индекси немаа што да придонесат, бидејќи работата што тие би ја вршеле веќе се вршеше. |
| | 1281 | |
| | 1282 | Четвртата е Амдаловиот закон. Единствениот индекс со мерлив ефект е `idx_ucp_enrollment_completed` кај `dashboard_course_performance()`. Тој го направи scan-от врз `user_course_progress` 2,6 пати побрз, од 120,9 на 46,2 ms, но бидејќи тој scan зафаќаше само околу 4% од вкупното време, крајната добивка е 4,3%. Забрзување на мал дел од работата дава мала вкупна добивка, колку и да е импресивен факторот на тој дел. |
| | 1283 | |
| | 1284 | Индексите не се бесплатни, бидејќи заземаат простор и го забавуваат секое `INSERT`, `UPDATE` и `DELETE`: |
| | 1285 | |
| | 1286 | || '''Индекс''' || '''Големина''' || '''Табела''' || '''Однос''' || |
| | 1287 | || `idx_payment_cov_all` || 38 MB || 52 MB || 73% || |
| | 1288 | || `idx_ucp_enrollment_completed` || 29 MB || 60 MB || 48% || |
| | 1289 | || `idx_review_cov` || 6,4 MB || 17 MB || 37% || |
| | 1290 | || `idx_enrollment_course_version_id` || 5,4 MB || 51 MB || 11% || |
| | 1291 | || `idx_expert_course_course_id` || 104 kB || — || — || |
| | 1292 | |
| | 1293 | `idx_payment_cov_all` зафаќа 73% од големината на самата табела, а донесе околу 2%. Тоа е лоша размена, бидејќи секое ново плаќање ќе мора да го одржува и тој индекс. |
| | 1294 | |
| | 1295 | Од сите тестирани индекси, вредни за задржување се само два: |
| | 1296 | |
| | 1297 | {{{ |
| | 1298 | -- Мерлива добивка кај dashboard_course_performance()\nCREATE INDEX idx_ucp_enrollment_completed\n ON user_course_progress(enrollment_id) INCLUDE (is_completed);\n\n-- Не помогна кај извештаите, но е единствениот непокриен foreign key.\n-- Вреди заради DELETE/UPDATE врз course_version и обични OLTP барања.\nCREATE INDEX idx_enrollment_course_version_id\n ON enrollment(course_version_id); |
| | 1299 | }}} |
| | 1300 | |
| | 1301 | Останатите, `idx_payment_cov_all`, `idx_review_cov`, `idx_payment_completed_covering`, `idx_enrollment_purchase_date_cov` и `idx_enrollment_cv_user`, не се препорачуваат, бидејќи цената во простор и во забавени записи е поголема од добивката од 1 до 2%. |
| | 1302 | |
| | 1303 | Мерењата покажаа дека надвор од индексите постојат значително поголеми добивки: |
| | 1304 | |
| | 1305 | || '''Интервенција''' || '''Ефект''' || '''Забелешка''' || |
| | 1306 | || Јазик како параметар на функција 2 || 3,3 пати побрзо || менува резултат, сите јазици се намерна одлука || |
| | 1307 | || Јазик како параметар на функција 3 || 1,7 пати побрзо || исто така менува резултат || |
| | 1308 | || Параметар за период кај функција 1 || 4,0 пати побрзо || измерено, ги прави индексите корисни || |
| | 1309 | || Пред-агрегирање пред join кај функција 4 || потенцијално голем || го спречува множењето на редови || |
| | 1310 | || `work_mem` 4 MB на 256 MB || 0% до −5% || не помогна, кај функција 3 дури штети || |
| | 1311 | || `jit = off` || околу ±3% || без доследен ефект || |
| | 1312 | |
| | 1313 | Најголемиот поединечен фактор во целата анализа не е индекс, туку бројот на редови што влегуваат во агрегацијата. Кај функциите 2 и 3 тој број е тројно поголем затоа што се враќаат сите три јазика, што е намерна одлука но со мерлива цена, бидејќи функција 2 е 3,3 пати побавна а функција 3 е 1,7 пати побавна од варијантите со еден јазик. Ако некогаш затреба извештај за еден јазик, најдобро е тоа да се направи преку параметар во функцијата, за филтерот да делува пред агрегацијата, бидејќи дополнително филтрирање врз готовиот резултат не носи никаква добивка кога целата работа веќе е завршена. |
| | 1314 | |
| | 1315 | Индексите се вредна алатка кога query-то бара мал дел од голема табела. Овие четири функции бараат сè, па индексот нема што да прескокне. Пред да се додаваат индекси, `EXPLAIN ANALYZE` треба да покаже дека времето навистина се троши на пристап до податоци, а овде тоа се троши на сортирање и агрегирање на редови што query-то само ги умножило. |