├── .gitignore ├── code ├── queries.sql └── schema.ddl └── slides ├── css └── sql-performance-explained-impress.css ├── img ├── Logo CartoDB Positive large 2.png ├── Logo_square_postgis.png ├── democracy-overrated.gif ├── f1.1-index-leaf-nodes.png ├── f1.2-b-tree-structure.png ├── f1.3-b-tree-traversal.png ├── f2.2-index-range.png ├── f2.3-index-range.png ├── f3.2-scalability-by-data-volume.png ├── f5.1-index-based-heap-table.png ├── f5.2-secondary-index-iot.png ├── f6.5-database-order-features.png ├── f7.2-offset-method.png ├── f7.3-seek-method.png ├── f7.4-paging-performance.png ├── idiomcoach.png ├── nacho.png ├── poll.png ├── postgresql-banner.png ├── postgresql-logo.png ├── sql-performance-explained-cover.png └── use-the-index-luke.png ├── index.html └── js └── impress.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /code/queries.sql: -------------------------------------------------------------------------------- 1 | explain analyze SELECT first_name, last_name 2 | FROM employees WHERE employee_id = 123; 3 | 4 | explain analyze SELECT first_name, last_name 5 | FROM employees WHERE employee_id = 111 and subsidiary_id = 333; 6 | 7 | explain analyze SELECT first_name, last_name 8 | FROM employees WHERE subsidiary_id = 333; 9 | 10 | explain analyze SELECT first_name, last_name 11 | FROM employees WHERE upper(last_name) = 'LN 38' 12 | -------------------------------------------------------------------------------- /code/schema.ddl: -------------------------------------------------------------------------------- 1 | \c postgres 2 | 3 | drop database sqlperf; 4 | create database sqlperf; 5 | 6 | \c sqlperf; 7 | 8 | drop table if exists employees; 9 | CREATE TABLE employees ( 10 | employee_id integer not null PRIMARY key, 11 | subsidiary_id integer not null, 12 | first_name text, 13 | last_name text, 14 | date_of_birth DATE NOT NULL, 15 | phone_number character varying(1000) NOT NULL, 16 | enabled boolean default true 17 | ); 18 | 19 | create unique index on employees (subsidiary_id, employee_id); 20 | 21 | --create index employees_last_name on employees(last_name); 22 | 23 | insert into employees (employee_id, subsidiary_id, first_name, last_name, date_of_birth, phone_number) 24 | select i, i % 100, 'FN ' || i, 'LN ' || i, date '01/01/1950' + i * interval '4 HOUR', 'PN' || i from generate_series(1, 100000) a(i); 25 | 26 | update employees set enabled = false where employee_id % 100 = 0; 27 | 28 | -- create index employees_upper_last_name on employees(upper(last_name)); 29 | 30 | -- create index employees_date on employees(date_of_birth); 31 | 32 | -- create index employees_date on employees(date_of_birth, subsidiary_id); 33 | 34 | -- create index employees_date on employees(subsidiary_id, date_of_birth); 35 | 36 | CREATE TABLE sales ( 37 | employee_id integer not null, 38 | subsidiary_id integer not null, 39 | sale_id bigint not null, 40 | amount integer, 41 | sale_date DATE NOT NULL 42 | ); 43 | 44 | alter table sales add primary key (employee_id, subsidiary_id, sale_id); 45 | 46 | alter table sales add constraint employee_sales foreign key (employee_id, subsidiary_id) references employees (employee_id, subsidiary_id); 47 | 48 | insert into sales (employee_id, subsidiary_id, sale_id, amount, sale_date) 49 | select (i / 10) + 1, ((i / 10) + 1) % 100, i, i + i, date '01/06/2016' - i * interval '10 minutes' from generate_series(1, 900000) a(i); 50 | 51 | -- create index employees_date on employees(date_of_birth, subsidiary_id); 52 | 53 | -- create index sales_date on sales(sale_date); 54 | -------------------------------------------------------------------------------- /slides/css/sql-performance-explained-impress.css: -------------------------------------------------------------------------------- 1 | /* Obviously inspired in impress-demo.css */ 2 | 3 | html, body, div, span, applet, object, iframe, 4 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 5 | a, abbr, acronym, address, big, cite, code, 6 | del, dfn, em, img, ins, kbd, q, s, samp, 7 | small, strike, strong, sub, sup, tt, var, 8 | b, u, i, center, 9 | dl, dt, dd, ol, ul, li, 10 | fieldset, form, label, legend, 11 | table, caption, tbody, tfoot, thead, tr, th, td, 12 | article, aside, canvas, details, embed, 13 | figure, figcaption, footer, header, hgroup, 14 | menu, nav, output, ruby, section, summary, 15 | time, mark, audio, video { 16 | margin: 0; 17 | padding: 0; 18 | border: 0; 19 | font-size: 100%; 20 | font: inherit; 21 | vertical-align: baseline; 22 | } 23 | 24 | /* HTML5 display-role reset for older browsers */ 25 | article, aside, details, figcaption, figure, 26 | footer, header, hgroup, menu, nav, section { 27 | display: block; 28 | } 29 | body { 30 | line-height: 1.5; 31 | } 32 | ol, ul { 33 | padding: inherit; 34 | line-height: 2; 35 | margin: inherit; 36 | } 37 | ul ul, ul ol, ol ol { 38 | margin-left: 40px; 39 | } 40 | blockquote, q { 41 | quotes: none; 42 | } 43 | blockquote:before, blockquote:after, 44 | q:before, q:after { 45 | content: ''; 46 | content: none; 47 | } 48 | 49 | blockquote p::before, blockquote p::after { 50 | content: '"'; 51 | } 52 | 53 | table { 54 | border-collapse: collapse; 55 | border-spacing: 0; 56 | } 57 | 58 | code { 59 | font-size: 80%; 60 | } 61 | 62 | .compressed code { 63 | font-size: 60%; 64 | } 65 | 66 | pre.code, code { 67 | text-align: left; 68 | } 69 | 70 | pre code.sql.hljs { 71 | padding-top: 0; 72 | padding-bottom: 0; 73 | line-height: 1.3; 74 | } 75 | 76 | pre + pre { 77 | margin-top: 10px; 78 | } 79 | 80 | .explanation { 81 | position: absolute; 82 | margin: 10%; 83 | padding: 2% 3%; 84 | top: 0; 85 | bottom: 0; 86 | left: 0; 87 | right: 0; 88 | background: rgba(255, 255, 0, 0.9); 89 | font-size: 140%; 90 | } 91 | 92 | ul.conclusion, .explanation { 93 | border-radius: 20px; 94 | } 95 | 96 | ul.alternatives li { 97 | list-style-type: lower-alpha;; 98 | } 99 | 100 | .source { 101 | display: inline-block; 102 | position: absolute; 103 | bottom: 20px; 104 | right: 20px; 105 | font-size: 60%; 106 | } 107 | 108 | .source::before { 109 | content: "["; 110 | } 111 | 112 | .source::after { 113 | content: "]"; 114 | } 115 | 116 | code .hljs-keyword { 117 | font-weight: normal; 118 | } 119 | 120 | code strong, code .hljs-keyword { 121 | color: #111; 122 | } 123 | 124 | body { 125 | font-family: 'Overlock', Helvetica, Arial, sans-serif; 126 | } 127 | 128 | .explanation { 129 | font-family: "Open Sans Condensed", sans-serif; 130 | } 131 | 132 | h1, h2, h3 { 133 | font-family: 'Overlock SC', Helvetica, Arial, sans-serif; 134 | } 135 | 136 | pre.code, code { 137 | font-family: 'Fira Mono', monospace; 138 | } 139 | 140 | body { 141 | color: black; 142 | min-height: 740px; 143 | 144 | /* 145 | background: rgb(215, 215, 215); 146 | background: -webkit-gradient(radial, 50% 50%, 0, 50% 50%, 500, from(rgb(240, 240, 240)), to(rgb(190, 190, 190))); 147 | background: -webkit-radial-gradient(rgb(240, 240, 240), rgb(190, 190, 190)); 148 | background: -moz-radial-gradient(rgb(240, 240, 240), rgb(190, 190, 190)); 149 | background: -ms-radial-gradient(rgb(240, 240, 240), rgb(190, 190, 190)); 150 | background: -o-radial-gradient(rgb(240, 240, 240), rgb(190, 190, 190)); 151 | background: radial-gradient(rgb(240, 240, 240), rgb(190, 190, 190)); 152 | */ 153 | background-color: white; 154 | } 155 | 156 | b, strong, dt { font-weight: bold } 157 | i, em { font-style: italic } 158 | 159 | dd::before { 160 | content: '-> ' 161 | } 162 | 163 | a { 164 | color: inherit; 165 | text-decoration: none; 166 | 167 | text-decoration: underline; 168 | 169 | -webkit-transition: 0.5s; 170 | -moz-transition: 0.5s; 171 | -ms-transition: 0.5s; 172 | -o-transition: 0.5s; 173 | transition: 0.5s; 174 | } 175 | 176 | a:hover, 177 | a:focus { 178 | background: rgba(255,255,255,.2); 179 | } 180 | 181 | h1 { 182 | white-space: nowrap; 183 | } 184 | 185 | p { 186 | text-align: left; 187 | } 188 | 189 | .step p { 190 | margin-top: 10px; 191 | margin-bottom: 40px; 192 | } 193 | 194 | .fallback-message { 195 | line-height: 1.3; 196 | 197 | width: 780px; 198 | padding: 10px 10px 0; 199 | margin: 20px auto; 200 | 201 | border: 1px solid #E4C652; 202 | border-radius: 10px; 203 | background: #EEDC94; 204 | } 205 | 206 | .fallback-message p { 207 | margin-bottom: 10px; 208 | } 209 | 210 | .impress-supported .fallback-message { 211 | display: none; 212 | } 213 | 214 | .step { 215 | position: relative; 216 | padding: 40px; 217 | margin: 20px auto; 218 | width: 1000px; 219 | height: 750px; 220 | 221 | -webkit-box-sizing: border-box; 222 | -moz-box-sizing: border-box; 223 | -ms-box-sizing: border-box; 224 | -o-box-sizing: border-box; 225 | box-sizing: border-box; 226 | 227 | font-size: 30px; 228 | line-height: 1.5; 229 | 230 | text-align: center; 231 | } 232 | 233 | .step ul, .step li, .step dl, .step dt, .step dd { 234 | text-align: left; 235 | } 236 | 237 | .step img { 238 | max-width: 100%; 239 | max-height: 600px; 240 | } 241 | 242 | .step.section h1 { 243 | font-size: 300%; 244 | } 245 | 246 | .step.description h1, .step.question h1, .step.answer h1 { 247 | padding-top: 0; 248 | padding-right: 40px; 249 | padding-left: 40px; 250 | 251 | margin-bottom: 10px; 252 | 253 | text-align: left; 254 | color: #444; 255 | } 256 | 257 | .step.section h1, .step.section h2, #intro h1, #intro h2 { 258 | text-shadow: 0px 13px 17px rgba(150, 150, 150, 0.8); 259 | } 260 | 261 | .step.description h1::before, .step.question h1::before, .step.answer h1::before { 262 | content: '· ·  '; 263 | } 264 | 265 | .step.description h1::after, .step.question h1::after, .step.answer h1::after { 266 | content: '  · · · · · · · · · · · · · · · · · · · · · · ·'; 267 | } 268 | 269 | .step.description.notes:not(.active), 270 | .step.question.notes:not(.active), 271 | .step.answer.notes:not(.active) { 272 | opacity: 0; 273 | } 274 | 275 | .wtf h1, .wtfit { 276 | color: red; 277 | } 278 | 279 | ul.conclusion { 280 | background: #111; 281 | color: white; 282 | padding-top: 0; 283 | padding-bottom: 0; 284 | } 285 | 286 | ul.conclusion li.good, ul.conclusion li.neutral, ul.conclusion li.bad { 287 | list-style: none; 288 | } 289 | 290 | ul.conclusion li.good::before { 291 | content: '👍 '; 292 | } 293 | 294 | ul.conclusion li.neutral::before { 295 | content: '≃ '; 296 | } 297 | 298 | ul.conclusion li.bad::before { 299 | content: '👎 '; 300 | } 301 | 302 | ul.conclusion li.bad .wtf { 303 | color: red; 304 | } 305 | 306 | #summary ol { 307 | margin-left: 100px; 308 | } 309 | 310 | .impress-enabled .step.answer { 311 | opacity: 0; 312 | } 313 | 314 | .impress-enabled .step { 315 | margin: 0; 316 | opacity: 0.1; 317 | 318 | -webkit-transition: opacity 1s; 319 | -moz-transition: opacity 1s; 320 | -ms-transition: opacity 1s; 321 | -o-transition: opacity 1s; 322 | transition: opacity 1s; 323 | } 324 | 325 | .impress-enabled .step.active { opacity: 1 } 326 | 327 | .impress-enabled .step.substep.active { opacity: 1 } 328 | 329 | .impress-enabled .step.substep:not(.active){ opacity: 0 } 330 | 331 | .slide { 332 | display: block; 333 | 334 | padding: 40px 60px; 335 | 336 | background-color: white; 337 | border: 1px solid rgba(0, 0, 0, .3); 338 | border-radius: 10px; 339 | box-shadow: 0 2px 6px rgba(0, 0, 0, .1); 340 | 341 | font-size: 40px; 342 | line-height: 36px; 343 | letter-spacing: -1px; 344 | } 345 | 346 | .slide q { 347 | display: block; 348 | font-size: 50px; 349 | line-height: 72px; 350 | 351 | margin-top: 100px; 352 | } 353 | 354 | .slide q strong { 355 | white-space: nowrap; 356 | } 357 | 358 | .footnote { 359 | position: absolute; 360 | bottom: 40px; 361 | font-size: 70%; 362 | font-style: italic; 363 | color: black; 364 | } 365 | 366 | .figure-caption::before { 367 | content: 'Figure: ' 368 | } 369 | 370 | .figure-caption { 371 | text-align: left; 372 | font-size: 90%; 373 | font-style: italic; 374 | } 375 | 376 | .full-image-background { 377 | padding: 0; 378 | background-size: auto 100%; 379 | background-repeat: no-repeat; 380 | } 381 | 382 | .full-image-background .content-wrapper { 383 | width: 100%; 384 | height: 100%; 385 | } 386 | 387 | .dark-image-background .content-wrapper { 388 | background: rgba(0, 0, 0, .5); 389 | } 390 | 391 | .dark-image-background .content-wrapper, .dark-image-background .content-wrapper .footnote { 392 | color: white; 393 | } 394 | 395 | .light-image-background .content-wrapper { 396 | background: rgba(255, 255, 255, .2); 397 | } 398 | 399 | .full-image-background .content { 400 | padding: 40px 60px; 401 | } 402 | 403 | 404 | .splash h1, .end h1 { 405 | font-size: 400%; 406 | text-align: center; 407 | -webkit-transform: rotate(-10deg); 408 | -moz-transform: rotate(-10deg); 409 | -ms-transform: rotate(-10deg); 410 | -o-transform: rotate(-10deg); 411 | transform: rotate(-10deg); 412 | } 413 | 414 | .highlight { 415 | display: inline-block; 416 | -webkit-transition: 0.1s; 417 | -moz-transition: 0.1s; 418 | -ms-transition: 0.1s; 419 | -o-transition: 0.1s; 420 | transition: 0.1s; 421 | 422 | -webkit-transition-delay: 2s; 423 | -moz-transition-delay: 2s; 424 | -ms-transition-delay: 2s; 425 | -o-transition-delay: 2s; 426 | transition-delay: 2s; 427 | 428 | text-shadow: 0px 0px 17px rgba(234, 150, 150, 1); 429 | } 430 | 431 | .present .highlight { 432 | -webkit-transform: rotate(-10deg); 433 | -moz-transform: rotate(-10deg); 434 | -ms-transform: rotate(-10deg); 435 | -o-transform: rotate(-10deg); 436 | transform: rotate(-10deg); 437 | } 438 | 439 | #intro { 440 | background-size: 100% 100%; 441 | background-color: white; 442 | padding: 40px; 443 | } 444 | 445 | #intro .metadata { 446 | background-color: rgba(255, 255, 255, 0.5); 447 | background-size: 30%; 448 | height: 100%; 449 | line-height: 2; 450 | border-radius: 0.2em; 451 | } 452 | 453 | #intro h1, #intro h2 { 454 | text-align: center; 455 | } 456 | 457 | #intro h1 { 458 | padding: 80px 40px 20px 40px; 459 | font-size: 130%; 460 | } 461 | 462 | #intro h2 { 463 | margin-bottom: 280px; 464 | } 465 | 466 | #intro .author { 467 | line-height: 1.5; 468 | text-align: right; 469 | font-size: 50%; 470 | margin-right: 40px; 471 | background: url('../img/idiomcoach.png') no-repeat 40px 0; 472 | background-size: 300px; 473 | } 474 | 475 | #intro .author a { 476 | display: block; 477 | } 478 | 479 | #author.juanignaciosl { 480 | background: url('../img/nacho.png') no-repeat 100% 40px; 481 | background-size: 350px; 482 | padding-right: 350px; 483 | } 484 | 485 | #author h1 { 486 | font-size: 80%; 487 | } 488 | 489 | #author .metadata { 490 | font-size: 60%; 491 | margin-bottom: 80px; 492 | } 493 | 494 | #author dl { 495 | font-size: 80%; 496 | } 497 | 498 | #author dl dt { 499 | font-weight: bold; 500 | float: left; 501 | clear: left; 502 | margin-right: 20px; 503 | margin-bottom: 10px; 504 | } 505 | 506 | #author dl dd { 507 | float: left; 508 | } 509 | 510 | .not-important { 511 | font-size: 70%; 512 | color: #666666; 513 | } 514 | 515 | .book-cover { 516 | max-height: 100%; 517 | } 518 | 519 | #overview:not(.active) { display: none } 520 | 521 | .impress-on-overview .step { 522 | opacity: 1; 523 | cursor: pointer; 524 | } 525 | 526 | 527 | /* 528 | Now, when we have all the steps styled let's give users a hint how to navigate 529 | around the presentation. 530 | 531 | The best way to do this would be to use JavaScript, show a delayed hint for a 532 | first time users, then hide it and store a status in cookie or localStorage... 533 | 534 | But I wanted to have some CSS fun and avoid additional scripting... 535 | 536 | Let me explain it first, so maybe the transition magic will be more readable 537 | when you read the code. 538 | 539 | First of all I wanted the hint to appear only when user is idle for a while. 540 | You can't detect the 'idle' state in CSS, but I delayed a appearing of the 541 | hint by 5s using transition-delay. 542 | 543 | You also can't detect in CSS if the user is a first-time visitor, so I had to 544 | make an assumption that I'll only show the hint on the first step. And when 545 | the step is changed hide the hint, because I can assume that user already 546 | knows how to navigate. 547 | 548 | To summarize it - hint is shown when the user is on the first step for longer 549 | than 5 seconds. 550 | 551 | The other problem I had was caused by the fact that I wanted the hint to fade 552 | in and out. It can be easily achieved by transitioning the opacity property. 553 | But that also meant that the hint was always on the screen, even if totally 554 | transparent. It covered part of the screen and you couldn't correctly clicked 555 | through it. 556 | Unfortunately you cannot transition between display `block` and `none` in pure 557 | CSS, so I needed a way to not only fade out the hint but also move it out of 558 | the screen. 559 | 560 | I solved this problem by positioning the hint below the bottom of the screen 561 | with CSS transform and moving it up to show it. But I also didn't want this move 562 | to be visible. I wanted the hint only to fade in and out visually, so I delayed 563 | the fade in transition, so it starts when the hint is already in its correct 564 | position on the screen. 565 | 566 | I know, it sounds complicated ... maybe it would be easier with the code? 567 | */ 568 | 569 | .hint { 570 | /* 571 | We hide the hint until presentation is started and from browsers not supporting 572 | impress.js, as they will have a linear scrollable view ... 573 | */ 574 | display: none; 575 | 576 | /* 577 | ... and give it some fixed position and nice styles. 578 | */ 579 | position: fixed; 580 | left: 0; 581 | right: 0; 582 | bottom: 200px; 583 | 584 | background: rgba(0,0,0,0.5); 585 | color: #EEE; 586 | text-align: center; 587 | 588 | font-size: 50px; 589 | padding: 20px; 590 | 591 | z-index: 100; 592 | 593 | /* 594 | By default we don't want the hint to be visible, so we make it transparent ... 595 | */ 596 | opacity: 0; 597 | 598 | /* 599 | ... and position it below the bottom of the screen (relative to it's fixed position) 600 | */ 601 | -webkit-transform: translateY(400px); 602 | -moz-transform: translateY(400px); 603 | -ms-transform: translateY(400px); 604 | -o-transform: translateY(400px); 605 | transform: translateY(400px); 606 | 607 | /* 608 | Now let's imagine that the hint is visible and we want to fade it out and move out 609 | of the screen. 610 | 611 | So we define the transition on the opacity property with 1s duration and another 612 | transition on transform property delayed by 1s so it will happen after the fade out 613 | on opacity finished. 614 | 615 | This way user will not see the hint moving down. 616 | */ 617 | -webkit-transition: opacity 1s, -webkit-transform 0.5s 1s; 618 | -moz-transition: opacity 1s, -moz-transform 0.5s 1s; 619 | -ms-transition: opacity 1s, -ms-transform 0.5s 1s; 620 | -o-transition: opacity 1s, -o-transform 0.5s 1s; 621 | transition: opacity 1s, transform 0.5s 1s; 622 | } 623 | 624 | /* 625 | Now we 'enable' the hint when presentation is initialized ... 626 | */ 627 | .impress-enabled .hint { display: block } 628 | 629 | /* 630 | ... and we will show it when the first step (with id 'bored') is active. 631 | */ 632 | .impress-on-bored .hint { 633 | /* 634 | We remove the transparency and position the hint in its default fixed 635 | position. 636 | */ 637 | opacity: 1; 638 | 639 | -webkit-transform: translateY(0px); 640 | -moz-transform: translateY(0px); 641 | -ms-transform: translateY(0px); 642 | -o-transform: translateY(0px); 643 | transform: translateY(0px); 644 | 645 | /* 646 | Now for fade in transition we have the oposite situation from the one 647 | above. 648 | 649 | First after 4.5s delay we animate the transform property to move the hint 650 | into its correct position and after that we fade it in with opacity 651 | transition. 652 | */ 653 | -webkit-transition: opacity 1s 5s, -webkit-transform 0.5s 4.5s; 654 | -moz-transition: opacity 1s 5s, -moz-transform 0.5s 4.5s; 655 | -ms-transition: opacity 1s 5s, -ms-transform 0.5s 4.5s; 656 | -o-transition: opacity 1s 5s, -o-transform 0.5s 4.5s; 657 | transition: opacity 1s 5s, transform 0.5s 4.5s; 658 | } 659 | 660 | /* 661 | And as the last thing there is a workaround for quite strange bug. 662 | It happens a lot in Chrome. I don't remember if I've seen it in Firefox. 663 | 664 | Sometimes the element positioned in 3D (especially when it's moved back 665 | along Z axis) is not clickable, because it falls 'behind' the 666 | element. 667 | 668 | To prevent this, I decided to make non clickable by setting 669 | pointer-events property to `none` value. 670 | Value if this property is inherited, so to make everything else clickable 671 | I bring it back on the #impress element. 672 | 673 | If you want to know more about `pointer-events` here are some docs: 674 | https://developer.mozilla.org/en/CSS/pointer-events 675 | 676 | There is one very important thing to notice about this workaround - it makes 677 | everything 'unclickable' except what's in #impress element. 678 | 679 | So use it wisely ... or don't use at all. 680 | */ 681 | .impress-enabled { pointer-events: none } 682 | .impress-enabled #impress { pointer-events: auto } 683 | 684 | /* 685 | There is one funny thing I just realized. 686 | 687 | Thanks to this workaround above everything except #impress element is invisible 688 | for click events. That means that the hint element is also not clickable. 689 | So basically all of this transforms and delayed transitions trickery was probably 690 | not needed at all... 691 | 692 | But it was fun to learn about it, wasn't it? 693 | */ 694 | 695 | /* 696 | That's all I have for you in this file. 697 | Thanks for reading. I hope you enjoyed it at least as much as I enjoyed writing it 698 | for you. 699 | */ 700 | -------------------------------------------------------------------------------- /slides/img/Logo CartoDB Positive large 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/Logo CartoDB Positive large 2.png -------------------------------------------------------------------------------- /slides/img/Logo_square_postgis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/Logo_square_postgis.png -------------------------------------------------------------------------------- /slides/img/democracy-overrated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/democracy-overrated.gif -------------------------------------------------------------------------------- /slides/img/f1.1-index-leaf-nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f1.1-index-leaf-nodes.png -------------------------------------------------------------------------------- /slides/img/f1.2-b-tree-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f1.2-b-tree-structure.png -------------------------------------------------------------------------------- /slides/img/f1.3-b-tree-traversal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f1.3-b-tree-traversal.png -------------------------------------------------------------------------------- /slides/img/f2.2-index-range.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f2.2-index-range.png -------------------------------------------------------------------------------- /slides/img/f2.3-index-range.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f2.3-index-range.png -------------------------------------------------------------------------------- /slides/img/f3.2-scalability-by-data-volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f3.2-scalability-by-data-volume.png -------------------------------------------------------------------------------- /slides/img/f5.1-index-based-heap-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f5.1-index-based-heap-table.png -------------------------------------------------------------------------------- /slides/img/f5.2-secondary-index-iot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f5.2-secondary-index-iot.png -------------------------------------------------------------------------------- /slides/img/f6.5-database-order-features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f6.5-database-order-features.png -------------------------------------------------------------------------------- /slides/img/f7.2-offset-method.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f7.2-offset-method.png -------------------------------------------------------------------------------- /slides/img/f7.3-seek-method.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f7.3-seek-method.png -------------------------------------------------------------------------------- /slides/img/f7.4-paging-performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/f7.4-paging-performance.png -------------------------------------------------------------------------------- /slides/img/idiomcoach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/idiomcoach.png -------------------------------------------------------------------------------- /slides/img/nacho.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/nacho.png -------------------------------------------------------------------------------- /slides/img/poll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/poll.png -------------------------------------------------------------------------------- /slides/img/postgresql-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/postgresql-banner.png -------------------------------------------------------------------------------- /slides/img/postgresql-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/postgresql-logo.png -------------------------------------------------------------------------------- /slides/img/sql-performance-explained-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/sql-performance-explained-cover.png -------------------------------------------------------------------------------- /slides/img/use-the-index-luke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanignaciosl/sql-performance-explained/bf7a957e26bd7e85980c79cfadda8baf32aca43f/slides/img/use-the-index-luke.png -------------------------------------------------------------------------------- /slides/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | SQL Performance Explained - Markus Winand | by Juan Ignacio Sánchez Lara @juanignaciosl 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |

Your browser doesn't support the features required by impress.js, so you are presented with a simplified version of this presentation.

27 |

For the best experience please use the latest Chrome, Safari or Firefox browser.

28 |
29 | 30 |
31 | 32 |
33 | 43 |
44 | 45 |
46 |
47 |

Juan Ignacio Sánchez Lara

48 | 52 | 53 |
54 |
UVa
I. T. Sistemas + I. Superior Informática
55 |
AVA, Telefónica, Thales
Developer
56 |
ITACyL
Software Analyst / Developer
57 |
CExC
Developer, CTO ...
58 |
CAG JCyL
Analyst
59 |
Compiled Cubes: inCitee
Cofounder / iOS Developer
60 |
CARTO
Backend Tech Lead
61 |
IDIOM COACH
Everything but proofreading
62 |
63 |
64 |

More information at personal and (newborn) develop blogs

65 |
66 | 67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 | 75 |
76 | SQL Performance Explained (book cover) 77 |
78 | 79 |
80 | 81 | Use the Index, Luke (web banner) 82 | 83 | 84 | Use the Index, Luke 85 | 86 | 87 | PostgreSQL manual 88 | 89 | 90 | PostgreSQL Manual 91 | 92 | 93 |

Slides: juanignaciosl.github.io/sql-performance-explained/slides

94 |
95 | 96 |
97 |

SQL Performance Explained

98 |
    99 |
  1. Book and web
  2. 100 |
  3. Anatomy of an Index
  4. 101 |
  5. The Where Clause
  6. 102 |
  7. Performance and Scalability
  8. 103 |
  9. The Join Operation
  10. 104 |
  11. Clustering Data
  12. 105 |
  13. Sorting and grouping
  14. 106 |
  15. Partial results
  16. 107 |
  17. Modifying data
  18. 108 |
  19. Execution plans
  20. 109 |
110 |
111 | 112 |
113 |

Anatomy of an Index

114 |
115 | 116 |
117 |

Anatomy of an Index

118 | 119 |
Index leaf nodes
120 |
121 | 122 |
123 |

Anatomy of an Index

124 | 140 |
141 | 142 |
143 |

Anatomy of an Index

144 | 145 |
Tree structure
146 |
147 | 148 |
149 |

Anatomy of an Index

150 | 156 |
157 | 158 |
159 |

Anatomy of an Index

160 | 161 |
Tree transversal
162 |
163 | 164 |
165 |

Anatomy of an Index

166 |

The first power of indexing

167 | 171 |
172 | 173 |
174 |

B-Trees: slowness

175 | 187 |
188 | 189 |
190 |

The Where Clause

191 |
192 | 193 |
194 |

The Where Clause

195 |

 196 | CREATE TABLE employees (
 197 |   employee_id integer not null PRIMARY key,
 198 |   subsidiary_id integer not null,
 199 |   first_name text,
 200 |   last_name text,
 201 |   date_of_birth DATE NOT NULL,
 202 |   phone_number character varying(1000) NOT NULL,
 203 |   enabled boolean default true
 204 | );
 205 | 
 206 | create unique index on employees (employee_id, subsidiary_id);
 207 | 
 208 | create index employees_last_name on employees(last_name);
 209 |       
210 |
211 | 212 |
213 |

The Where Clause

214 |

 215 |         Table "public.employees"
 216 | Column     |          Type           |  Modifiers
 217 | ---------------+-------------------------+--------------
 218 | employee_id   | integer                 | not null
 219 | subsidiary_id | integer                 | not null
 220 | first_name    | text                    |
 221 | last_name     | text                    |
 222 | date_of_birth | date                    | not null
 223 | phone_number  | character varying(1000) | not null
 224 | enabled       | boolean                 | default true
 225 | Indexes:
 226 | "employees_pkey" PRIMARY KEY, btree (employee_id)
 227 | "enabled_employees" btree (last_name) WHERE enabled = true
 228 | "employees_last_name" btree (last_name)
 229 |       
230 |
231 | 232 |
233 |

Where clause: indexes - equality

234 |

 235 | explain analyze SELECT first_name, last_name
 236 | FROM employees WHERE employee_id = 123;
 237 |       
238 |
239 | 240 |
241 |

Where clause: indexes - equality

242 |

 243 | explain analyze SELECT first_name, last_name
 244 | FROM employees WHERE employee_id = 123;
 245 |       
246 |

 247 | Index Scan using employees_pkey on employees
 248 |     (cost=0.29..8.31 rows=1 width=16)
 249 |     (actual time=0.009..0.010 rows=1 loops=1)
 250 | Index Cond: (employee_id = 123)
 251 | Planning time: 0.310 ms
 252 | Execution time: 0.051 ms
 253 |       
254 |
255 | 256 |
257 |

Where clause: indexes - multiple columns

258 |

 259 | explain analyze SELECT first_name, last_name
 260 | FROM employees WHERE employee_id = 111
 261 |     and subsidiary_id = 333;
 262 |       
263 |
264 | 265 |
266 |

Where clause: indexes - multiple columns

267 |

 268 | explain analyze SELECT first_name, last_name
 269 | FROM employees WHERE employee_id = 111
 270 |     and subsidiary_id = 333;
 271 |       
272 |

 273 | Index Scan using employees_employee_id_subsidiary_id_idx
 274 |     on employees (cost=0.29..8.31 rows=1 width=16)
 275 |     (actual time=0.023..0.025 rows=1 loops=1)
 276 | Index Cond: ((employee_id = 111) AND (subsidiary_id = 333))
 277 | Planning time: 0.130 ms
 278 | Execution time: 0.056 ms
 279 |       
280 |
281 | 282 |
283 |

Where clause: indexes - multiple columns

284 |

 285 | explain analyze SELECT first_name, last_name
 286 | FROM employees WHERE subsidiary_id = 333;
 287 |       
288 |
289 | 290 |
291 |

Where clause: indexes - multiple columns

292 |

 293 | explain analyze SELECT first_name, last_name
 294 | FROM employees WHERE subsidiary_id = 333;
 295 |       
296 |

 297 | Index Scan using employees_employee_id_subsidiary_id_idx
 298 |     on employees (cost=0.29..1858.30 rows=1 width=16)
 299 |     (actual time=0.034..3.498 rows=1 loops=1)
 300 |   Index Cond: (subsidiary_id = 333)
 301 | Planning time: 0.101 ms
 302 | Execution time: 3.527 ms
 303 |       
304 |
305 | 306 |
307 |

Where clause: indexes - multiple columns

308 |

 309 | explain analyze SELECT first_name, last_name
 310 | FROM employees WHERE subsidiary_id = 333;
 311 |       
312 |

 313 | Index Scan using employees_employee_id_subsidiary_id_idx
 314 |     on employees (cost=0.29..1858.30 rows=1 width=16)
 315 |     (actual time=0.034..3.498 rows=1 loops=1)
 316 |   Index Cond: (subsidiary_id = 333)
 317 | Planning time: 0.101 ms
 318 | Execution time: 3.527 ms
 319 |       
320 |
321 |

A multicolumn B-tree index can be used with query conditions that involve any subset of the index's columns, 322 | but the index is most efficient when there are constraints on the leading (leftmost) columns.

323 | PostgreSQL manual 324 |
325 |
326 | 327 |
328 |

Where clause: indexes - multiple columns

329 |

 330 | select * from employees where date_of_birth > TO_DATE($1, 'YYYY-MM-DD')
 331 |     and date_of_birth < TO_DATE($2, 'YYYY-MM-DD') and subsidiary_id = $3;
 332 |       
333 |

 334 | create index employees_date on
 335 |     employees(date_of_birth, subsidiary_id);
 336 |       
337 |

 338 | create index employees_date on
 339 |     employees(subsidiary_id, date_of_birth);
 340 |       
341 |
342 | 343 |
344 |

Where clause: indexes - multiple columns

345 | 346 |
347 | 348 |
349 |

Where clause: indexes - multiple columns

350 | 351 |
352 | 353 |
354 |

Where clause: indexes - multiple columns

355 |

 356 | Bitmap Heap Scan on employees
 357 |     (cost=14.54..22.28 rows=2 width=592) (actual time=0.069..0.071 rows=4 loops=1)
 358 |   Recheck Cond: ((date_of_birth > '1082-01-01'::date)
 359 |       AND (date_of_birth < '1083-01-01'::date) AND (subsidiary_id = 50))
 360 |   Heap Blocks: exact=3
 361 |   ->  Bitmap Index Scan on employees_date  (cost=0.00..14.54 rows=2 width=0)
 362 |       (actual time=0.064..0.064 rows=4 loops=1)
 363 |         Index Cond: ((date_of_birth > '1082-01-01'::date)
 364 |             AND (date_of_birth < '1083-01-01'::date) AND (subsidiary_id = 50))
 365 | Planning time: 0.365 ms
 366 | Execution time: 0.099 ms
 367 |       
368 |

 369 | Bitmap Heap Scan on employees
 370 |     (cost=4.34..19.58 rows=4 width=35) (actual time=0.045..0.051 rows=4 loops=1)
 371 |   Recheck Cond: ((subsidiary_id = 50) AND (date_of_birth > '1082-01-01'::date)
 372 |       AND (date_of_birth < '1083-01-01'::date))
 373 |   Heap Blocks: exact=3
 374 |   ->  Bitmap Index Scan on employees_date  (cost=0.00..4.34 rows=4 width=0)
 375 |       (actual time=0.037..0.037 rows=4 loops=1)
 376 |         Index Cond: ((subsidiary_id = 50) AND (date_of_birth > '1082-01-01'::date)
 377 |             AND (date_of_birth < '1083-01-01'::date))
 378 | Planning time: 0.481 ms
 379 | Execution time: 0.084 ms
 380 |       
381 |
382 | 383 |
384 |

Where clause: indexes - multiple columns

385 |

 386 | Bitmap Heap Scan on employees
 387 |     (cost=14.54..22.28 rows=2 width=592) (actual time=0.069..0.071 rows=4 loops=1)
 388 |   Recheck Cond: ((date_of_birth > '1082-01-01'::date)
 389 |       AND (date_of_birth < '1083-01-01'::date) AND (subsidiary_id = 50))
 390 |   Heap Blocks: exact=3
 391 |   ->  Bitmap Index Scan on employees_date  (cost=0.00..14.54 rows=2 width=0)
 392 |       (actual time=0.064..0.064 rows=4 loops=1)
 393 |         Index Cond: ((date_of_birth > '1082-01-01'::date)
 394 |             AND (date_of_birth < '1083-01-01'::date) AND (subsidiary_id = 50))
 395 | Planning time: 0.365 ms
 396 | Execution time: 0.099 ms
 397 |       
398 |

 399 | Bitmap Heap Scan on employees
 400 |     (cost=4.34..19.58 rows=4 width=35) (actual time=0.045..0.051 rows=4 loops=1)
 401 |   Recheck Cond: ((subsidiary_id = 50) AND (date_of_birth > '1082-01-01'::date)
 402 |       AND (date_of_birth < '1083-01-01'::date))
 403 |   Heap Blocks: exact=3
 404 |   ->  Bitmap Index Scan on employees_date  (cost=0.00..4.34 rows=4 width=0)
 405 |       (actual time=0.037..0.037 rows=4 loops=1)
 406 |         Index Cond: ((subsidiary_id = 50) AND (date_of_birth > '1082-01-01'::date)
 407 |             AND (date_of_birth < '1083-01-01'::date))
 408 | Planning time: 0.481 ms
 409 | Execution time: 0.084 ms
 410 |       
411 |
412 | The child plan node visits an index to find the locations 413 | of rows matching the index condition, and then the upper plan node actually fetches those rows from the table 414 | itself. Fetching rows separately is much more expensive than reading them sequentially, but because not all the 415 | pages of the table have to be visited, this is still cheaper than a sequential scan. 416 | PostgreSQL manual 417 |
418 |
419 | 420 |
421 |

Where clause: indexes - functions

422 |

 423 | explain analyze SELECT first_name, last_name
 424 | FROM employees WHERE last_name = 'LN 38'
 425 |       
426 |

 427 | Index Scan using employees_last_name on employees
 428 |     (cost=0.42..8.44 rows=1 width=16)
 429 |     (actual time=0.049..0.049 rows=1 loops=1)
 430 |   Index Cond: (last_name = 'LN 38'::text)
 431 | Planning time: 0.286 ms
 432 | Execution time: 0.082 ms
 433 |       
434 |
435 | 436 |
437 |

Where clause: indexes - functions

438 |

 439 | explain analyze SELECT first_name, last_name
 440 | FROM employees WHERE upper(last_name) = 'LN 38'
 441 |       
442 |

 443 | Seq Scan on employees
 444 |     (cost=0.00..2334.00 rows=500 width=16)
 445 |     (actual time=0.048..46.647 rows=1 loops=1)
 446 | Filter: (upper(last_name) = 'LN 38'::text)
 447 | Rows Removed by Filter: 99999
 448 | Planning time: 0.092 ms
 449 | Execution time: 46.672 ms
 450 |       
451 |
452 | 453 |
454 |

Where clause: indexes - functions

455 |

 456 | create index employees_upper_last_name
 457 |     on employees(upper(last_name));
 458 |       
459 |
460 | 461 |
462 |

Where clause: indexes - functions

463 |

 464 | explain analyze SELECT first_name, last_name
 465 | FROM employees WHERE upper(last_name) = 'LN 38'
 466 |       
467 |

 468 | Bitmap Heap Scan on employees
 469 |     (cost=12.29..775.05 rows=500 width=16)
 470 |     (actual time=0.070..0.070 rows=1 loops=1)
 471 |   Recheck Cond: (upper(last_name) = 'LN 38'::text)
 472 |   Heap Blocks: exact=1
 473 |   ->  Bitmap Index Scan on employees_upper_last_name
 474 |             (cost=0.00..12.17 rows=500 width=0)
 475 |             (actual time=0.065..0.065 rows=1 loops=1)
 476 |         Index Cond: (upper(last_name) = 'LN 38'::text)
 477 | Planning time: 0.360 ms
 478 | Execution time: 0.091 ms
 479 |       
480 |
481 | 482 |
483 |

Where clause: indexes - functions

484 |

 485 | explain analyze select * from employees where employee_id = 5;
 486 |       
487 |

 488 | explain analyze select * from employees where employee_id + 1 = 6;
 489 |       
490 |
491 | 492 |
493 |

Where clause: indexes - functions

494 |

 495 | explain analyze select * from employees where employee_id = 5;
 496 |       
497 |

 498 | Index Scan using employees_pkey on employees  (cost=0.29..8.31 rows=1 width=36) (actual time=0.014..0.015 rows=1 loops=1)
 499 |  Index Cond: (employee_id = 5)
 500 | Planning time: 0.075 ms
 501 | Execution time: 0.034 ms
 502 |       
503 |

 504 | explain analyze select * from employees where employee_id + 1 = 6;
 505 |       
506 |

 507 | Seq Scan on employees  (cost=0.00..2434.00 rows=500 width=36) (actual time=0.014..15.385 rows=1 loops=1)
 508 |  Filter: ((employee_id + 1) = 6)
 509 |  Rows Removed by Filter: 99999
 510 | Planning time: 0.066 ms
 511 | Execution time: 15.407 ms
 512 |       
513 |
514 | 515 |
516 |

Where clause: indexes - partial indexes

517 |

Partial indexes

518 |

 519 | create index enabled_employees on employees(last_name)
 520 |     where enabled = 't';
 521 |       
522 |
523 | 524 |
525 |

Where clause: indexes - parameterized queries

526 |

 527 | int subsidiary_id;
 528 | PreparedStatement command = connection.prepareStatement(
 529 |   "select first_name from employees where subsidiary_id = ?"
 530 | );
 531 | command.setInt(1, subsidiary_id);
 532 |       
533 | 540 |
541 | 542 |
543 |

Where clause: indexes - wrapping it up

544 | 554 |
555 | 556 |
557 |

Where clause: indexes - wrapping it up

558 | 569 |
570 | 571 |
572 |

Performance and
Scalability

573 |
574 | 575 |
576 |

Performance and Scalability

577 | 578 |
Scalability by data volume
579 |
580 | 581 |
582 |

The Join Operation

583 |
584 | 585 |
586 |

The Join Operation

587 | 594 | 595 |
596 | 597 |
598 |

The Join Operation: Nested Loops

599 | 604 |
605 | 606 |
607 |

The Join Operation: Nested Loops

608 | 613 | 617 |
618 | 619 |
620 |

The Join Operation: Hash Join

621 | 626 |
627 | 628 |
629 |

The Join Operation: Hash Join

630 | 635 | 639 |
640 | 641 |
642 |

The Join Operation: Sort Merge

643 | 647 |
648 | 649 |
650 |

The Join Operation: Sort Merge

651 | 655 | 659 |
660 | 661 |
662 |

Clustering Data

663 |

The Second Power of Indexing

664 |
665 | 666 |
667 |

Clustering Data: Index Filter Predicates

668 |
669 |
Clustering data
Store consecutively accessed data closely so that it requires fewer IO operations
670 |
671 | 676 |
677 | 678 |
679 |

Clustering Data: Index Filter Predicates

680 |

 681 | SELECT first_name, last_name, subsidiary_id, phone_number
 682 | FROM employees
 683 | WHERE subsidiary_id = ?
 684 | AND UPPER(last_name) LIKE '%INA%';
 685 |       
686 |

 687 | --------------------------------------------------------------
 688 | |Id | Operation | Name | Rows | Cost |
 689 | --------------------------------------------------------------
 690 | | 0 | SELECT STATEMENT | | 17 | 230 |
 691 | |*1 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | 17 | 230 |
 692 | |*2 | INDEX RANGE SCAN | EMPLOYEE_PK| 333 | 2 |
 693 | --------------------------------------------------------------
 694 | Predicate Information (identified by operation id):
 695 | ---------------------------------------------------
 696 |      1 - filter(UPPER("LAST_NAME") LIKE '%INA%')
 697 |      2 - access("SUBSIDIARY_ID"=TO_NUMBER(:A))
 698 |       
699 |
700 | 701 |
702 |

Clustering Data: Index Filter Predicates

703 |

 704 | CREATE INDEX empsubupnam ON employees
 705 |     (subsidiary_id, UPPER(last_name));
 706 |       
707 |
708 | 709 |
710 |

Clustering Data: Index Filter Predicates

711 |

 712 | CREATE INDEX empsubupnam ON employees
 713 |     (subsidiary_id, UPPER(last_name));
 714 |       
715 |

 716 | --------------------------------------------------------------
 717 | |Id | Operation                   | Name       | Rows | Cost |
 718 | --------------------------------------------------------------
 719 | | 0 | SELECT STATEMENT            |            |   17 |   20 |
 720 | | 1 |  TABLE ACCESS BY INDEX ROWID| EMPLOYEES  |   17 |   20 |
 721 | |*2 |   INDEX RANGE SCAN          | EMPSUBUPNAM|   17 |    3 |
 722 | --------------------------------------------------------------
 723 | Predicate Information (identified by operation id):
 724 | ---------------------------------------------------
 725 |    2 - access("SUBSIDIARY_ID"=TO_NUMBER(:A))
 726 |        filter(UPPER("LAST_NAME") LIKE '%INA%')
 727 |       
728 |
729 | 730 |
731 |

Clustering Data: Index-only scan

732 | 735 |
736 | 737 |
738 |

Clustering Data: Index-only scan

739 |

 740 | explain select subsidiary_id, last_name
 741 |     from employees where subsidiary_id < 5;
 742 |       
743 |

 744 | Bitmap Heap Scan on employees
 745 |     (cost=100.20..1089.58 rows=5150 width=12)
 746 |     (actual time=1.102..3.429 rows=5000 loops=1)
 747 |   Recheck Cond: (subsidiary_id < 5)
 748 |   Heap Blocks: exact=925
 749 |   ->  Bitmap Index Scan on employees_subsidiary_id_employee_id_idx
 750 |           (cost=0.00..98.92 rows=5150 width=0)
 751 |           (actual time=0.853..0.853 rows=5000 loops=1)
 752 |         Index Cond: (subsidiary_id < 5)
 753 | Planning time: 0.178 ms
 754 | Execution time: 3.867 ms
 755 |       
756 |
757 | 758 |
759 |

Clustering Data: Index-only scan

760 |

 761 | CREATE INDEX empsubupnam ON employees
 762 |     (subsidiary_id, last_name);
 763 |       
764 |
765 | 766 |
767 |

Clustering Data: Index-only scan

768 |

 769 | CREATE INDEX empsubupnam ON employees
 770 |     (subsidiary_id, last_name);
 771 |       
772 |

 773 | explain select subsidiary_id, last_name
 774 |     from employees where subsidiary_id < 5;
 775 |       
776 |

 777 | Index Only Scan using empsubupnam on employees
 778 |       (cost=0.42..170.54 rows=5150 width=12)
 779 |       (actual time=0.109..0.912 rows=5000 loops=1)
 780 |  Index Cond: (subsidiary_id < 5)
 781 |  Heap Fetches: 0
 782 | Planning time: 0.091 ms
 783 | Execution time: 1.244 ms
 784 |       
785 |
786 | 787 |
788 |

Clustering Data: Index-organized Tables

789 |
790 |
Index-organized Tables (aka clustered index)
791 |
Using an index as primary table store
792 |
A B-tree index without a heap table
793 |
794 | 799 | 800 |
801 | 802 |
803 |

Clustering Data: Index-organized Tables

804 | 805 |
Index-based heap table
806 |
807 | 808 |
809 |

Clustering Data: Index-organized Tables

810 | 811 |
Secondary Index on an IOT
812 |
813 | 814 |
815 |

Clustering data - wrapping it up

816 | 821 |
822 | 823 |
824 |

Sorting and grouping

825 |
826 | 827 |
828 |

Sorting and grouping

829 | 834 |
835 | 836 |
837 |

Sorting and grouping

838 |

 839 | explain analyze select * from sales order by sale_date;
 840 |       
841 |

 842 | Sort  (cost=122200.04..124450.04 rows=900000 width=24)
 843 |     (actual time=810.872..1007.881 rows=900000 loops=1)
 844 |  Sort Key: sale_date
 845 |  Sort Method: external merge  Disk: 29912kB
 846 |  ->  Seq Scan on sales
 847 |     (cost=0.00..14733.00 rows=900000 width=24)
 848 |     (actual time=0.006..113.583 rows=900000 loops=1)
 849 | Planning time: 0.112 ms
 850 | Execution time: 1074.673 ms
 851 |       
852 |
853 | 854 |
855 |

Sorting and grouping

856 |

 857 | create index sales_date on sales(sale_date);
 858 |       
859 |

 860 | explain analyze select * from sales order by sale_date;
 861 |       
862 |
863 | 864 |
865 |

Sorting and grouping

866 |

 867 | create index sales_date on sales(sale_date);
 868 |       
869 |

 870 | explain analyze select * from sales order by sale_date;
 871 |       
872 |

 873 | Index Scan using sales_date on sales
 874 |     (cost=0.42..29120.43 rows=900000 width=24)
 875 |     (actual time=0.024..247.604 rows=900000 loops=1)
 876 | Planning time: 0.118 ms
 877 | Execution time: 315.568 ms
 878 |       
879 |
880 | 881 |
882 |

Sorting and grouping

883 | 887 |
888 | 889 |
890 |

Sorting and grouping

891 | 896 |
897 | 898 |
899 |

Sorting and grouping

900 | 905 |

TL;DR: cost is not always an accurate measure

906 |

For a query that requires scanning a large fraction of the table, an explicit sort is likely to be faster than 907 | using an index because it requires less disk I/O due to following a sequential access pattern

908 |

Cost can increase because clustering factor of the new index is worse

909 |
910 | 911 |
912 |

[Clustering Factor]

913 |
914 | 915 |
916 |

[Clustering Factor]

917 | 925 |
926 | 927 |
928 |

[Clustering Factor]

929 |

 930 | select tablename, attname, correlation from pg_stats where
 931 |     schemaname = 'public';
 932 |       
933 |

 934 |  tablename |    attname    | correlation
 935 | -----------+---------------+-------------
 936 |  employees | date_of_birth |    0.976956
 937 |  employees | phone_number  |    0.801827
 938 |  employees | enabled       |    0.935313
 939 |  sales     | employee_id   |           1
 940 |  sales     | subsidiary_id |   0.0180426
 941 |  sales     | sale_id       |           1
 942 |  sales     | amount        |           1
 943 |  sales     | sale_date     |          -1
 944 |  employees | employee_id   |    0.976954
 945 |  employees | subsidiary_id |   -0.022466
 946 |  employees | first_name    |    0.801827
 947 |  employees | last_name     |    0.801827
 948 |       
949 |
950 | 951 |
952 |

Sorting and grouping: asc/desc and nulls

953 | 958 |
959 | 960 |
961 |

Sorting and grouping: grouping

962 | Algorithms 963 | 967 |
968 | 969 |
970 |

Sorting and grouping: grouping

971 | Algorithms 972 | 976 | 979 |
980 | 981 |
982 |

Partial results

983 |
984 | 985 |
986 |

Partial results: retrieving top-n

987 | 990 |
991 | 992 |
993 |

Partial results: retrieving top-n

994 | 997 |
998 | 999 |
1000 |

Partial results: retrieving top-n

1001 | 1004 | 1005 | PostgreSQL 1006 |

1007 | LIMIT { count | ALL }
1008 | OFFSET start
1009 |       
1010 | 1011 | SQL:2008 1012 |

1013 | OFFSET start { ROW | ROWS }
1014 | FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY
1015 |       
1016 |
1017 | 1018 |
1019 |

Partial results: paging - offset method

1020 |

1021 | SELECT * FROM sales ORDER BY sale_date DESC OFFSET 10
1022 | FETCH NEXT 10 ROWS ONLY;
1023 |       
1024 | 1025 |

The offset method

1026 |
1027 | 1028 |
1029 |

Partial results: paging - seek method

1030 |

1031 | SELECT * FROM sales WHERE sale_date < ?
1032 | ORDER BY sale_date DESC FETCH FIRST 10 ROWS ONLY;
1033 |       
1034 | 1035 |

The seek method

1036 |
1037 | 1038 |
1039 |

Partial results: paging

1040 | 1041 |

Offset vs seek performance

1042 |
1043 | 1044 |
1045 |

Partial results: paging

1046 | 1064 |
1065 | 1066 |
1067 |

Modifying data

1068 |
1069 | 1070 |
1071 |

Modifying data: insert

1072 | The more indexes a table has, the slower the execution becomes. 1073 |
    1074 |
  1. Store the row in any block with free space
  2. 1075 |
  3. 1076 | Update indexes 1077 |
      1078 |
    1. Find the correct leaf node
    2. 1079 |
    3. Split leaf node if it has no free space
    4. 1080 |
    1081 |
  4. 1082 |
1083 |
1084 | 1085 |
1086 |

Modifying data: insert

1087 | The more indexes a table has, the slower the execution becomes. 1088 |
    1089 |
  1. Store the row in any block with free space
  2. 1090 |
  3. 1091 | Update indexes 1092 |
      1093 |
    1. Find the correct leaf node
    2. 1094 |
    3. Split leaf node if it has no free space
    4. 1095 |
    1096 |
  4. 1097 |
1098 | 1099 | 1103 | Corollary: drop indexes on batch inserts 1104 |
1105 | 1106 |
1107 |

Modifying data: delete

1108 | Similar to insert but, since it also searches, it takes advantage of indexes. 1109 |
    1110 |
  1. Find matches
  2. 1111 |
  3. Delete the row
  4. 1112 |
  5. Update indexes
  6. 1113 |
1114 |
1115 | 1116 |
1117 |

Modifying data: delete

1118 | Similar to insert but, since it also searches, it takes advantage of indexes. 1119 |
    1120 |
  1. Find matches
  2. 1121 |
  3. Delete the row
  4. 1122 |
  5. Update indexes
  6. 1123 |
1124 | 1125 | 1130 | 1131 | PS: keep MVCC in mind!!! 1132 |
1133 | 1134 |
1135 |

Modifying data: update

1136 | 1141 |
1142 | 1143 |
1144 |

Execution plans

1145 |

PostgreSQL

1146 |
1147 | 1148 |
1149 |

Execution plans

1150 |

1151 | PREPARE stmt(int) AS SELECT $1;
1152 |       
1153 |
1154 | 1155 |
1156 |

Execution plans

1157 |

1158 | PREPARE stmt(int) AS SELECT $1;
1159 |       
1160 |

1161 | EXPLAIN EXECUTE stmt(1);
1162 |       
1163 |
1164 | 1165 |
1166 |

Execution plans

1167 |

1168 | PREPARE stmt(int) AS SELECT $1;
1169 |       
1170 |

1171 | EXPLAIN EXECUTE stmt(1);
1172 |       
1173 |

1174 | Result  (cost=0.00..0.01 rows=1 width=0)
1175 |       
1176 |
1177 |
0.00
Startup cost
1178 |
0.01
Total cost for the execution if all rows are retrieved
1179 |
1180 |
1181 | 1182 |
1183 |

Execution plans

1184 |

1185 | PREPARE stmt(int) AS SELECT $1;
1186 |       
1187 |

1188 | EXPLAIN EXECUTE stmt(1);
1189 |       
1190 |

1191 | Result  (cost=0.00..0.01 rows=1 width=0)
1192 |       
1193 |
1194 |
0.00
Startup cost
1195 |
0.01
Total cost for the execution if all rows are retrieved
1196 |
1197 |

1198 | DEALLOCATE stmt;
1199 |       
1200 |
1201 | 1202 |
1203 |

Execution plans

1204 |

1205 | EXPLAIN ANALYZE EXECUTE stmt(1);
1206 |       
1207 | Careful, It actually runs the query! 1208 |

1209 | EXPLAIN ANALYZE UPDATE EMPLOYEES
1210 |     SET LAST_NAME = 'x' WHERE EMPLOYEE_ID = 1;
1211 |       
1212 |

1213 | Update on employees  (cost=0.29..8.31 rows=1 width=34)
1214 |     (actual time=0.261..0.261 rows=0 loops=1)
1215 |   ->  Index Scan using employees_pkey on employees
1216 |       (cost=0.29..8.31 rows=1 width=34)
1217 |       (actual time=0.136..0.138 rows=1 loops=1)
1218 |         Index Cond: (employee_id = 1)
1219 | Planning time: 0.095 ms
1220 | Execution time: 0.297 ms
1221 |       
1222 |
1223 | 1224 |
1225 |

Execution plans: index and table access

1226 |
1227 |
Seq Scan
1228 |
Index Scan
1229 |
Index Only Scan
1230 |
Bitmap Index Scan / Bitmap Heap Scan / Recheck Cond
1231 |
1232 | 1233 |
1234 | 1235 |
1236 |

Execution plans: Join Operations

1237 |
1238 |
Nested Loops
1239 |
Hash Join / Hash
1240 |
Merge Join
1241 |
1242 |
1243 | 1244 |
1245 |

Execution plans: Sorting and Grouping

1246 |
1247 |
Sort / Sort Key
1248 |
GroupAggregate
1249 |
HashAggregate
1250 |
1251 |
1252 | 1253 |
1254 |

Execution plans: Top-N Queries

1255 |
1256 |
Limit
1257 |
WindowAgg
1258 |
1259 |
1260 | 1261 |
1262 |

Execution plans: Distinguishing Access and Filter-Predicates

1263 |
1264 |
Access Predicate (“Index Cond”)
1265 |
Start and stop conditions of the leaf node traversal
1266 |
Index Filter Predicate (“Index Cond”)
1267 |
Applied during the leaf node traversal only. They do not contribute to the start and stop conditions and do not narrow the scanned range
1268 |
Table level filter predicate (“Filter”)
1269 |
Predicates on columns that are not part of the index are evaluated on the table level
1270 |
1271 |

PostgreSQL execution plans do not show index access and filter predicates separately—both show up as “Index Cond”. That means the execution plan must be compared to the index definition to differentiate access predicates from index filter predicates.

1272 |
1273 | 1274 |
1275 |

Execution plans: Distinguishing Access and Filter-Predicates

1276 |

1277 | CREATE TABLE scale_data (
1278 |      section NUMERIC NOT NULL,
1279 |      id1     NUMERIC NOT NULL,
1280 |      id2     NUMERIC NOT NULL
1281 |   );
1282 | CREATE INDEX scale_data_key ON scale_data(section, id1);
1283 |       
1284 |
1285 | 1286 |
1287 |

Execution plans: Distinguishing Access and Filter-Predicates

1288 |

1289 | PREPARE stmt(int) AS SELECT count(*)
1290 |                          FROM scale_data
1291 |                         WHERE section = 1
1292 | AND id2 = $1; EXPLAIN EXECUTE stmt(1);
1293 |       
1294 |

1295 |  Aggregate  (cost=529346.31..529346.32 rows=1 width=0)
1296 |     Output: count(*)
1297 |     -> Index Scan using scale_data_key on scale_data
1298 |        (cost=0.00..529338.83 rows=2989 width=0)
1299 |        Index Cond: (scale_data.section = 1::numeric)
1300 |        Filter: (scale_data.id2 = ($1)::numeric)
1301 |       
1302 |
1303 | 1304 |
1305 |

Execution plans: Distinguishing Access and Filter-Predicates

1306 |

1307 | CREATE INDEX scale_slow
1308 |             ON scale_data (section, id1, id2);
1309 |       
1310 |

1311 | Aggregate (cost=14215.98..14215.99 rows=1 width=0)
1312 |     Output: count(*)
1313 | -> Index Scan using scale_slow on scale_data
1314 | (cost=0.00..14208.51 rows=2989 width=0)
1315 | Index Cond: (section = 1::numeric AND id2 = ($1)::numeric)
1316 |       
1317 |
1318 | 1319 |
1320 |

Questions?

1321 |

Slides & content available soon at juanignaciosl.github.io

1322 |
1323 | 1324 |
1325 |

Thank you
very much!

1326 |

Slides & content available soon at juanignaciosl.github.io

1327 |
1328 | 1329 |
1330 | 1331 |
1332 |

Use a spacebar or arrow keys to navigate

1333 |
1334 | 1339 | 1340 | 1341 | 1342 | 1343 | 1344 | 1345 | -------------------------------------------------------------------------------- /slides/js/impress.js: -------------------------------------------------------------------------------- 1 | /** 2 | * impress.js 3 | * 4 | * impress.js is a presentation tool based on the power of CSS3 transforms and transitions 5 | * in modern browsers and inspired by the idea behind prezi.com. 6 | * 7 | * 8 | * Copyright 2011-2012 Bartek Szopka (@bartaz) 9 | * 10 | * Released under the MIT and GPL Licenses. 11 | * 12 | * ------------------------------------------------ 13 | * author: Bartek Szopka 14 | * version: 0.5.3 15 | * url: http://bartaz.github.com/impress.js/ 16 | * source: http://github.com/bartaz/impress.js/ 17 | */ 18 | 19 | /*jshint bitwise:true, curly:true, eqeqeq:true, forin:true, latedef:true, newcap:true, 20 | noarg:true, noempty:true, undef:true, strict:true, browser:true */ 21 | 22 | // You are one of those who like to know how things work inside? 23 | // Let me show you the cogs that make impress.js run... 24 | ( function( document, window ) { 25 | "use strict"; 26 | 27 | // HELPER FUNCTIONS 28 | 29 | // `pfx` is a function that takes a standard CSS property name as a parameter 30 | // and returns it's prefixed version valid for current browser it runs in. 31 | // The code is heavily inspired by Modernizr http://www.modernizr.com/ 32 | var pfx = ( function() { 33 | 34 | var style = document.createElement( "dummy" ).style, 35 | prefixes = "Webkit Moz O ms Khtml".split( " " ), 36 | memory = {}; 37 | 38 | return function( prop ) { 39 | if ( typeof memory[ prop ] === "undefined" ) { 40 | 41 | var ucProp = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ), 42 | props = ( prop + " " + prefixes.join( ucProp + " " ) + ucProp ).split( " " ); 43 | 44 | memory[ prop ] = null; 45 | for ( var i in props ) { 46 | if ( style[ props[ i ] ] !== undefined ) { 47 | memory[ prop ] = props[ i ]; 48 | break; 49 | } 50 | } 51 | 52 | } 53 | 54 | return memory[ prop ]; 55 | }; 56 | 57 | } )(); 58 | 59 | // `arraify` takes an array-like object and turns it into real Array 60 | // to make all the Array.prototype goodness available. 61 | var arrayify = function( a ) { 62 | return [].slice.call( a ); 63 | }; 64 | 65 | // `css` function applies the styles given in `props` object to the element 66 | // given as `el`. It runs all property names through `pfx` function to make 67 | // sure proper prefixed version of the property is used. 68 | var css = function( el, props ) { 69 | var key, pkey; 70 | for ( key in props ) { 71 | if ( props.hasOwnProperty( key ) ) { 72 | pkey = pfx( key ); 73 | if ( pkey !== null ) { 74 | el.style[ pkey ] = props[ key ]; 75 | } 76 | } 77 | } 78 | return el; 79 | }; 80 | 81 | // `toNumber` takes a value given as `numeric` parameter and tries to turn 82 | // it into a number. If it is not possible it returns 0 (or other value 83 | // given as `fallback`). 84 | var toNumber = function( numeric, fallback ) { 85 | return isNaN( numeric ) ? ( fallback || 0 ) : Number( numeric ); 86 | }; 87 | 88 | // `byId` returns element with given `id` - you probably have guessed that ;) 89 | var byId = function( id ) { 90 | return document.getElementById( id ); 91 | }; 92 | 93 | // `$` returns first element for given CSS `selector` in the `context` of 94 | // the given element or whole document. 95 | var $ = function( selector, context ) { 96 | context = context || document; 97 | return context.querySelector( selector ); 98 | }; 99 | 100 | // `$$` return an array of elements for given CSS `selector` in the `context` of 101 | // the given element or whole document. 102 | var $$ = function( selector, context ) { 103 | context = context || document; 104 | return arrayify( context.querySelectorAll( selector ) ); 105 | }; 106 | 107 | // `triggerEvent` builds a custom DOM event with given `eventName` and `detail` data 108 | // and triggers it on element given as `el`. 109 | var triggerEvent = function( el, eventName, detail ) { 110 | var event = document.createEvent( "CustomEvent" ); 111 | event.initCustomEvent( eventName, true, true, detail ); 112 | el.dispatchEvent( event ); 113 | }; 114 | 115 | // `translate` builds a translate transform string for given data. 116 | var translate = function( t ) { 117 | return " translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) "; 118 | }; 119 | 120 | // `rotate` builds a rotate transform string for given data. 121 | // By default the rotations are in X Y Z order that can be reverted by passing `true` 122 | // as second parameter. 123 | var rotate = function( r, revert ) { 124 | var rX = " rotateX(" + r.x + "deg) ", 125 | rY = " rotateY(" + r.y + "deg) ", 126 | rZ = " rotateZ(" + r.z + "deg) "; 127 | 128 | return revert ? rZ + rY + rX : rX + rY + rZ; 129 | }; 130 | 131 | // `scale` builds a scale transform string for given data. 132 | var scale = function( s ) { 133 | return " scale(" + s + ") "; 134 | }; 135 | 136 | // `perspective` builds a perspective transform string for given data. 137 | var perspective = function( p ) { 138 | return " perspective(" + p + "px) "; 139 | }; 140 | 141 | // `getElementFromHash` returns an element located by id from hash part of 142 | // window location. 143 | var getElementFromHash = function() { 144 | 145 | // Get id from url # by removing `#` or `#/` from the beginning, 146 | // so both "fallback" `#slide-id` and "enhanced" `#/slide-id` will work 147 | return byId( window.location.hash.replace( /^#\/?/, "" ) ); 148 | }; 149 | 150 | // `computeWindowScale` counts the scale factor between window size and size 151 | // defined for the presentation in the config. 152 | var computeWindowScale = function( config ) { 153 | var hScale = window.innerHeight / config.height, 154 | wScale = window.innerWidth / config.width, 155 | scale = hScale > wScale ? wScale : hScale; 156 | 157 | if ( config.maxScale && scale > config.maxScale ) { 158 | scale = config.maxScale; 159 | } 160 | 161 | if ( config.minScale && scale < config.minScale ) { 162 | scale = config.minScale; 163 | } 164 | 165 | return scale; 166 | }; 167 | 168 | // CHECK SUPPORT 169 | var body = document.body; 170 | 171 | var ua = navigator.userAgent.toLowerCase(); 172 | var impressSupported = 173 | 174 | // Browser should support CSS 3D transtorms 175 | ( pfx( "perspective" ) !== null ) && 176 | 177 | // Browser should support `classList` and `dataset` APIs 178 | ( body.classList ) && 179 | ( body.dataset ) && 180 | 181 | // But some mobile devices need to be blacklisted, 182 | // because their CSS 3D support or hardware is not 183 | // good enough to run impress.js properly, sorry... 184 | ( ua.search( /(iphone)|(ipod)|(android)/ ) === -1 ); 185 | 186 | if ( !impressSupported ) { 187 | 188 | // We can't be sure that `classList` is supported 189 | body.className += " impress-not-supported "; 190 | } else { 191 | body.classList.remove( "impress-not-supported" ); 192 | body.classList.add( "impress-supported" ); 193 | } 194 | 195 | // GLOBALS AND DEFAULTS 196 | 197 | // This is where the root elements of all impress.js instances will be kept. 198 | // Yes, this means you can have more than one instance on a page, but I'm not 199 | // sure if it makes any sense in practice ;) 200 | var roots = {}; 201 | 202 | // Some default config values. 203 | var defaults = { 204 | width: 1024, 205 | height: 768, 206 | maxScale: 1, 207 | minScale: 0, 208 | 209 | perspective: 1000, 210 | 211 | transitionDuration: 1000 212 | }; 213 | 214 | // It's just an empty function ... and a useless comment. 215 | var empty = function() { return false; }; 216 | 217 | // IMPRESS.JS API 218 | 219 | // And that's where interesting things will start to happen. 220 | // It's the core `impress` function that returns the impress.js API 221 | // for a presentation based on the element with given id ('impress' 222 | // by default). 223 | var impress = window.impress = function( rootId ) { 224 | 225 | // If impress.js is not supported by the browser return a dummy API 226 | // it may not be a perfect solution but we return early and avoid 227 | // running code that may use features not implemented in the browser. 228 | if ( !impressSupported ) { 229 | return { 230 | init: empty, 231 | goto: empty, 232 | prev: empty, 233 | next: empty 234 | }; 235 | } 236 | 237 | rootId = rootId || "impress"; 238 | 239 | // If given root is already initialized just return the API 240 | if ( roots[ "impress-root-" + rootId ] ) { 241 | return roots[ "impress-root-" + rootId ]; 242 | } 243 | 244 | // Data of all presentation steps 245 | var stepsData = {}; 246 | 247 | // Element of currently active step 248 | var activeStep = null; 249 | 250 | // Current state (position, rotation and scale) of the presentation 251 | var currentState = null; 252 | 253 | // Array of step elements 254 | var steps = null; 255 | 256 | // Configuration options 257 | var config = null; 258 | 259 | // Scale factor of the browser window 260 | var windowScale = null; 261 | 262 | // Root presentation elements 263 | var root = byId( rootId ); 264 | var canvas = document.createElement( "div" ); 265 | 266 | var initialized = false; 267 | 268 | // STEP EVENTS 269 | // 270 | // There are currently two step events triggered by impress.js 271 | // `impress:stepenter` is triggered when the step is shown on the 272 | // screen (the transition from the previous one is finished) and 273 | // `impress:stepleave` is triggered when the step is left (the 274 | // transition to next step just starts). 275 | 276 | // Reference to last entered step 277 | var lastEntered = null; 278 | 279 | // `onStepEnter` is called whenever the step element is entered 280 | // but the event is triggered only if the step is different than 281 | // last entered step. 282 | var onStepEnter = function( step ) { 283 | if ( lastEntered !== step ) { 284 | triggerEvent( step, "impress:stepenter" ); 285 | lastEntered = step; 286 | } 287 | }; 288 | 289 | // `onStepLeave` is called whenever the step element is left 290 | // but the event is triggered only if the step is the same as 291 | // last entered step. 292 | var onStepLeave = function( step ) { 293 | if ( lastEntered === step ) { 294 | triggerEvent( step, "impress:stepleave" ); 295 | lastEntered = null; 296 | } 297 | }; 298 | 299 | // `initStep` initializes given step element by reading data from its 300 | // data attributes and setting correct styles. 301 | var initStep = function( el, idx ) { 302 | var data = el.dataset, 303 | step = { 304 | translate: { 305 | x: toNumber( data.x ), 306 | y: toNumber( data.y ), 307 | z: toNumber( data.z ) 308 | }, 309 | rotate: { 310 | x: toNumber( data.rotateX ), 311 | y: toNumber( data.rotateY ), 312 | z: toNumber( data.rotateZ || data.rotate ) 313 | }, 314 | scale: toNumber( data.scale, 1 ), 315 | el: el 316 | }; 317 | 318 | if ( !el.id ) { 319 | el.id = "step-" + ( idx + 1 ); 320 | } 321 | 322 | stepsData[ "impress-" + el.id ] = step; 323 | 324 | css( el, { 325 | position: "absolute", 326 | transform: "translate(-50%,-50%)" + 327 | translate( step.translate ) + 328 | rotate( step.rotate ) + 329 | scale( step.scale ), 330 | transformStyle: "preserve-3d" 331 | } ); 332 | }; 333 | 334 | // `init` API function that initializes (and runs) the presentation. 335 | var init = function() { 336 | if ( initialized ) { return; } 337 | 338 | // First we set up the viewport for mobile devices. 339 | // For some reason iPad goes nuts when it is not done properly. 340 | var meta = $( "meta[name='viewport']" ) || document.createElement( "meta" ); 341 | meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no"; 342 | if ( meta.parentNode !== document.head ) { 343 | meta.name = "viewport"; 344 | document.head.appendChild( meta ); 345 | } 346 | 347 | // Initialize configuration object 348 | var rootData = root.dataset; 349 | config = { 350 | width: toNumber( rootData.width, defaults.width ), 351 | height: toNumber( rootData.height, defaults.height ), 352 | maxScale: toNumber( rootData.maxScale, defaults.maxScale ), 353 | minScale: toNumber( rootData.minScale, defaults.minScale ), 354 | perspective: toNumber( rootData.perspective, defaults.perspective ), 355 | transitionDuration: toNumber( 356 | rootData.transitionDuration, defaults.transitionDuration 357 | ) 358 | }; 359 | 360 | windowScale = computeWindowScale( config ); 361 | 362 | // Wrap steps with "canvas" element 363 | arrayify( root.childNodes ).forEach( function( el ) { 364 | canvas.appendChild( el ); 365 | } ); 366 | root.appendChild( canvas ); 367 | 368 | // Set initial styles 369 | document.documentElement.style.height = "100%"; 370 | 371 | css( body, { 372 | height: "100%", 373 | overflow: "hidden" 374 | } ); 375 | 376 | var rootStyles = { 377 | position: "absolute", 378 | transformOrigin: "top left", 379 | transition: "all 0s ease-in-out", 380 | transformStyle: "preserve-3d" 381 | }; 382 | 383 | css( root, rootStyles ); 384 | css( root, { 385 | top: "50%", 386 | left: "50%", 387 | transform: perspective( config.perspective / windowScale ) + scale( windowScale ) 388 | } ); 389 | css( canvas, rootStyles ); 390 | 391 | body.classList.remove( "impress-disabled" ); 392 | body.classList.add( "impress-enabled" ); 393 | 394 | // Get and init steps 395 | steps = $$( ".step", root ); 396 | steps.forEach( initStep ); 397 | 398 | // Set a default initial state of the canvas 399 | currentState = { 400 | translate: { x: 0, y: 0, z: 0 }, 401 | rotate: { x: 0, y: 0, z: 0 }, 402 | scale: 1 403 | }; 404 | 405 | initialized = true; 406 | 407 | triggerEvent( root, "impress:init", { api: roots[ "impress-root-" + rootId ] } ); 408 | }; 409 | 410 | // `getStep` is a helper function that returns a step element defined by parameter. 411 | // If a number is given, step with index given by the number is returned, if a string 412 | // is given step element with such id is returned, if DOM element is given it is returned 413 | // if it is a correct step element. 414 | var getStep = function( step ) { 415 | if ( typeof step === "number" ) { 416 | step = step < 0 ? steps[ steps.length + step ] : steps[ step ]; 417 | } else if ( typeof step === "string" ) { 418 | step = byId( step ); 419 | } 420 | return ( step && step.id && stepsData[ "impress-" + step.id ] ) ? step : null; 421 | }; 422 | 423 | // Used to reset timeout for `impress:stepenter` event 424 | var stepEnterTimeout = null; 425 | 426 | // `goto` API function that moves to step given with `el` parameter 427 | // (by index, id or element), with a transition `duration` optionally 428 | // given as second parameter. 429 | var goto = function( el, duration ) { 430 | 431 | if ( !initialized || !( el = getStep( el ) ) ) { 432 | 433 | // Presentation not initialized or given element is not a step 434 | return false; 435 | } 436 | 437 | // Sometimes it's possible to trigger focus on first link with some keyboard action. 438 | // Browser in such a case tries to scroll the page to make this element visible 439 | // (even that body overflow is set to hidden) and it breaks our careful positioning. 440 | // 441 | // So, as a lousy (and lazy) workaround we will make the page scroll back to the top 442 | // whenever slide is selected 443 | // 444 | // If you are reading this and know any better way to handle it, I'll be glad to hear 445 | // about it! 446 | window.scrollTo( 0, 0 ); 447 | 448 | var step = stepsData[ "impress-" + el.id ]; 449 | 450 | if ( activeStep ) { 451 | activeStep.classList.remove( "active" ); 452 | body.classList.remove( "impress-on-" + activeStep.id ); 453 | } 454 | el.classList.add( "active" ); 455 | 456 | body.classList.add( "impress-on-" + el.id ); 457 | 458 | // Compute target state of the canvas based on given step 459 | var target = { 460 | rotate: { 461 | x: -step.rotate.x, 462 | y: -step.rotate.y, 463 | z: -step.rotate.z 464 | }, 465 | translate: { 466 | x: -step.translate.x, 467 | y: -step.translate.y, 468 | z: -step.translate.z 469 | }, 470 | scale: 1 / step.scale 471 | }; 472 | 473 | // Check if the transition is zooming in or not. 474 | // 475 | // This information is used to alter the transition style: 476 | // when we are zooming in - we start with move and rotate transition 477 | // and the scaling is delayed, but when we are zooming out we start 478 | // with scaling down and move and rotation are delayed. 479 | var zoomin = target.scale >= currentState.scale; 480 | 481 | duration = toNumber( duration, config.transitionDuration ); 482 | var delay = ( duration / 2 ); 483 | 484 | // If the same step is re-selected, force computing window scaling, 485 | // because it is likely to be caused by window resize 486 | if ( el === activeStep ) { 487 | windowScale = computeWindowScale( config ); 488 | } 489 | 490 | var targetScale = target.scale * windowScale; 491 | 492 | // Trigger leave of currently active element (if it's not the same step again) 493 | if ( activeStep && activeStep !== el ) { 494 | onStepLeave( activeStep ); 495 | } 496 | 497 | // Now we alter transforms of `root` and `canvas` to trigger transitions. 498 | // 499 | // And here is why there are two elements: `root` and `canvas` - they are 500 | // being animated separately: 501 | // `root` is used for scaling and `canvas` for translate and rotations. 502 | // Transitions on them are triggered with different delays (to make 503 | // visually nice and 'natural' looking transitions), so we need to know 504 | // that both of them are finished. 505 | css( root, { 506 | 507 | // To keep the perspective look similar for different scales 508 | // we need to 'scale' the perspective, too 509 | transform: perspective( config.perspective / targetScale ) + scale( targetScale ), 510 | transitionDuration: duration + "ms", 511 | transitionDelay: ( zoomin ? delay : 0 ) + "ms" 512 | } ); 513 | 514 | css( canvas, { 515 | transform: rotate( target.rotate, true ) + translate( target.translate ), 516 | transitionDuration: duration + "ms", 517 | transitionDelay: ( zoomin ? 0 : delay ) + "ms" 518 | } ); 519 | 520 | // Here is a tricky part... 521 | // 522 | // If there is no change in scale or no change in rotation and translation, it means 523 | // there was actually no delay - because there was no transition on `root` or `canvas` 524 | // elements. We want to trigger `impress:stepenter` event in the correct moment, so 525 | // here we compare the current and target values to check if delay should be taken into 526 | // account. 527 | // 528 | // I know that this `if` statement looks scary, but it's pretty simple when you know 529 | // what is going on 530 | // - it's simply comparing all the values. 531 | if ( currentState.scale === target.scale || 532 | ( currentState.rotate.x === target.rotate.x && 533 | currentState.rotate.y === target.rotate.y && 534 | currentState.rotate.z === target.rotate.z && 535 | currentState.translate.x === target.translate.x && 536 | currentState.translate.y === target.translate.y && 537 | currentState.translate.z === target.translate.z ) ) { 538 | delay = 0; 539 | } 540 | 541 | // Store current state 542 | currentState = target; 543 | activeStep = el; 544 | 545 | // And here is where we trigger `impress:stepenter` event. 546 | // We simply set up a timeout to fire it taking transition duration 547 | // (and possible delay) into account. 548 | // 549 | // I really wanted to make it in more elegant way. The `transitionend` event seemed to 550 | // be the best way to do it, but the fact that I'm using transitions on two separate 551 | // elements and that the `transitionend` event is only triggered when there was a 552 | // transition (change in the values) caused some bugs and made the code really 553 | // complicated, cause I had to handle all the conditions separately. And it still 554 | // needed a `setTimeout` fallback for the situations when there is no transition at 555 | // all. 556 | // So I decided that I'd rather make the code simpler than use shiny new 557 | // `transitionend`. 558 | // 559 | // If you want learn something interesting and see how it was done with `transitionend` 560 | // go back to 561 | // version 0.5.2 of impress.js: 562 | // http://github.com/bartaz/impress.js/blob/0.5.2/js/impress.js 563 | window.clearTimeout( stepEnterTimeout ); 564 | stepEnterTimeout = window.setTimeout( function() { 565 | onStepEnter( activeStep ); 566 | }, duration + delay ); 567 | 568 | return el; 569 | }; 570 | 571 | // `prev` API function goes to previous step (in document order) 572 | var prev = function() { 573 | var prev = steps.indexOf( activeStep ) - 1; 574 | prev = prev >= 0 ? steps[ prev ] : steps[ steps.length - 1 ]; 575 | 576 | return goto( prev ); 577 | }; 578 | 579 | // `next` API function goes to next step (in document order) 580 | var next = function() { 581 | var next = steps.indexOf( activeStep ) + 1; 582 | next = next < steps.length ? steps[ next ] : steps[ 0 ]; 583 | 584 | return goto( next ); 585 | }; 586 | 587 | // Adding some useful classes to step elements. 588 | // 589 | // All the steps that have not been shown yet are given `future` class. 590 | // When the step is entered the `future` class is removed and the `present` 591 | // class is given. When the step is left `present` class is replaced with 592 | // `past` class. 593 | // 594 | // So every step element is always in one of three possible states: 595 | // `future`, `present` and `past`. 596 | // 597 | // There classes can be used in CSS to style different types of steps. 598 | // For example the `present` class can be used to trigger some custom 599 | // animations when step is shown. 600 | root.addEventListener( "impress:init", function() { 601 | 602 | // STEP CLASSES 603 | steps.forEach( function( step ) { 604 | step.classList.add( "future" ); 605 | } ); 606 | 607 | root.addEventListener( "impress:stepenter", function( event ) { 608 | event.target.classList.remove( "past" ); 609 | event.target.classList.remove( "future" ); 610 | event.target.classList.add( "present" ); 611 | }, false ); 612 | 613 | root.addEventListener( "impress:stepleave", function( event ) { 614 | event.target.classList.remove( "present" ); 615 | event.target.classList.add( "past" ); 616 | }, false ); 617 | 618 | }, false ); 619 | 620 | // Adding hash change support. 621 | root.addEventListener( "impress:init", function() { 622 | 623 | // Last hash detected 624 | var lastHash = ""; 625 | 626 | // `#/step-id` is used instead of `#step-id` to prevent default browser 627 | // scrolling to element in hash. 628 | // 629 | // And it has to be set after animation finishes, because in Chrome it 630 | // makes transtion laggy. 631 | // BUG: http://code.google.com/p/chromium/issues/detail?id=62820 632 | root.addEventListener( "impress:stepenter", function( event ) { 633 | window.location.hash = lastHash = "#/" + event.target.id; 634 | }, false ); 635 | 636 | window.addEventListener( "hashchange", function() { 637 | 638 | // When the step is entered hash in the location is updated 639 | // (just few lines above from here), so the hash change is 640 | // triggered and we would call `goto` again on the same element. 641 | // 642 | // To avoid this we store last entered hash and compare. 643 | if ( window.location.hash !== lastHash ) { 644 | goto( getElementFromHash() ); 645 | } 646 | }, false ); 647 | 648 | // START 649 | // by selecting step defined in url or first step of the presentation 650 | goto( getElementFromHash() || steps[ 0 ], 0 ); 651 | }, false ); 652 | 653 | body.classList.add( "impress-disabled" ); 654 | 655 | // Store and return API for given impress.js root element 656 | return ( roots[ "impress-root-" + rootId ] = { 657 | init: init, 658 | goto: goto, 659 | next: next, 660 | prev: prev 661 | } ); 662 | 663 | }; 664 | 665 | // Flag that can be used in JS to check if browser have passed the support test 666 | impress.supported = impressSupported; 667 | 668 | } )( document, window ); 669 | 670 | // NAVIGATION EVENTS 671 | 672 | // As you can see this part is separate from the impress.js core code. 673 | // It's because these navigation actions only need what impress.js provides with 674 | // its simple API. 675 | // 676 | // In future I think about moving it to make them optional, move to separate files 677 | // and treat more like a 'plugins'. 678 | ( function( document, window ) { 679 | "use strict"; 680 | 681 | // Throttling function calls, by Remy Sharp 682 | // http://remysharp.com/2010/07/21/throttling-function-calls/ 683 | var throttle = function( fn, delay ) { 684 | var timer = null; 685 | return function() { 686 | var context = this, args = arguments; 687 | clearTimeout( timer ); 688 | timer = setTimeout( function() { 689 | fn.apply( context, args ); 690 | }, delay ); 691 | }; 692 | }; 693 | 694 | // Wait for impress.js to be initialized 695 | document.addEventListener( "impress:init", function( event ) { 696 | 697 | // Getting API from event data. 698 | // So you don't event need to know what is the id of the root element 699 | // or anything. `impress:init` event data gives you everything you 700 | // need to control the presentation that was just initialized. 701 | var api = event.detail.api; 702 | 703 | // KEYBOARD NAVIGATION HANDLERS 704 | 705 | // Prevent default keydown action when one of supported key is pressed. 706 | document.addEventListener( "keydown", function( event ) { 707 | if ( event.keyCode === 9 || 708 | ( event.keyCode >= 32 && event.keyCode <= 34 ) || 709 | ( event.keyCode >= 37 && event.keyCode <= 40 ) ) { 710 | event.preventDefault(); 711 | } 712 | }, false ); 713 | 714 | // Trigger impress action (next or prev) on keyup. 715 | 716 | // Supported keys are: 717 | // [space] - quite common in presentation software to move forward 718 | // [up] [right] / [down] [left] - again common and natural addition, 719 | // [pgdown] / [pgup] - often triggered by remote controllers, 720 | // [tab] - this one is quite controversial, but the reason it ended up on 721 | // this list is quite an interesting story... Remember that strange part 722 | // in the impress.js code where window is scrolled to 0,0 on every presentation 723 | // step, because sometimes browser scrolls viewport because of the focused element? 724 | // Well, the [tab] key by default navigates around focusable elements, so clicking 725 | // it very often caused scrolling to focused element and breaking impress.js 726 | // positioning. I didn't want to just prevent this default action, so I used [tab] 727 | // as another way to moving to next step... And yes, I know that for the sake of 728 | // consistency I should add [shift+tab] as opposite action... 729 | document.addEventListener( "keyup", function( event ) { 730 | 731 | if ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ) { 732 | return; 733 | } 734 | 735 | if ( event.keyCode === 9 || 736 | ( event.keyCode >= 32 && event.keyCode <= 34 ) || 737 | ( event.keyCode >= 37 && event.keyCode <= 40 ) ) { 738 | switch ( event.keyCode ) { 739 | case 33: // Page up 740 | case 37: // Left 741 | case 38: // Up 742 | api.prev(); 743 | break; 744 | case 9: // Tab 745 | case 32: // Space 746 | case 34: // Page down 747 | case 39: // Right 748 | case 40: // Down 749 | api.next(); 750 | break; 751 | } 752 | 753 | event.preventDefault(); 754 | } 755 | }, false ); 756 | 757 | // Delegated handler for clicking on the links to presentation steps 758 | document.addEventListener( "click", function( event ) { 759 | 760 | // Event delegation with "bubbling" 761 | // Check if event target (or any of its parents is a link) 762 | var target = event.target; 763 | while ( ( target.tagName !== "A" ) && 764 | ( target !== document.documentElement ) ) { 765 | target = target.parentNode; 766 | } 767 | 768 | if ( target.tagName === "A" ) { 769 | var href = target.getAttribute( "href" ); 770 | 771 | // If it's a link to presentation step, target this step 772 | if ( href && href[ 0 ] === "#" ) { 773 | target = document.getElementById( href.slice( 1 ) ); 774 | } 775 | } 776 | 777 | if ( api.goto( target ) ) { 778 | event.stopImmediatePropagation(); 779 | event.preventDefault(); 780 | } 781 | }, false ); 782 | 783 | // Delegated handler for clicking on step elements 784 | document.addEventListener( "click", function( event ) { 785 | var target = event.target; 786 | 787 | // Find closest step element that is not active 788 | while ( !( target.classList.contains( "step" ) && 789 | !target.classList.contains( "active" ) ) && 790 | ( target !== document.documentElement ) ) { 791 | target = target.parentNode; 792 | } 793 | 794 | if ( api.goto( target ) ) { 795 | event.preventDefault(); 796 | } 797 | }, false ); 798 | 799 | // Touch handler to detect taps on the left and right side of the screen 800 | // based on awesome work of @hakimel: https://github.com/hakimel/reveal.js 801 | document.addEventListener( "touchstart", function( event ) { 802 | if ( event.touches.length === 1 ) { 803 | var x = event.touches[ 0 ].clientX, 804 | width = window.innerWidth * 0.3, 805 | result = null; 806 | 807 | if ( x < width ) { 808 | result = api.prev(); 809 | } else if ( x > window.innerWidth - width ) { 810 | result = api.next(); 811 | } 812 | 813 | if ( result ) { 814 | event.preventDefault(); 815 | } 816 | } 817 | }, false ); 818 | 819 | // Rescale presentation when window is resized 820 | window.addEventListener( "resize", throttle( function() { 821 | 822 | // Force going to active step again, to trigger rescaling 823 | api.goto( document.querySelector( ".step.active" ), 500 ); 824 | }, 250 ), false ); 825 | 826 | }, false ); 827 | 828 | } )( document, window ); 829 | 830 | // THAT'S ALL FOLKS! 831 | // 832 | // Thanks for reading it all. 833 | // Or thanks for scrolling down and reading the last part. 834 | // 835 | // I've learnt a lot when building impress.js and I hope this code and comments 836 | // will help somebody learn at least some part of it. 837 | --------------------------------------------------------------------------------