PGDay UK 2016 -- Performace for queries with grouping

71
1/50 PostgreSQL Performance for queries with grouping Alexey Bashtanov, Brandwatch, Brighton 05 Jul 2016

Transcript of PGDay UK 2016 -- Performace for queries with grouping

Page 1: PGDay UK 2016 -- Performace for queries with grouping

1/50

PostgreSQLPerformance for queries with grouping

Alexey Bashtanov, Brandwatch, Brighton

05 Jul 2016

Page 2: PGDay UK 2016 -- Performace for queries with grouping

2/50

What is it all about?

This talk will cover optimisation ofGroupingAggregation

Unfortunately it will not cover optimisation ofGetting the dataFilteringJoinsWindow functionsOther data transformations

Page 3: PGDay UK 2016 -- Performace for queries with grouping

3/50

Outline

1 What is a grouping?

2 How does it work?Aggregation functions under the hoodGrouping algorithms

3 Optimisation: avoid sortsSimple group-byCount distinct valuesOrdered aggregates

4 Summation optimisation

5 Denormalized data aggregation

6 Arg-maximum

7 Some magic: loose index scan

8 The future: parallel execution

Page 4: PGDay UK 2016 -- Performace for queries with grouping

4/50

What is a grouping?

Page 5: PGDay UK 2016 -- Performace for queries with grouping

5/50

What is a grouping?

What do we call a grouping/aggregation operation?

An operation of splitting input data into several classes andthen compilation each class into one row.

332 21 1

33

3

3

1

1

2

2 152

2

2

23

3

1

1 83

32

23

31

1 9

Page 6: PGDay UK 2016 -- Performace for queries with grouping

6/50

Examples

SELECT department_id,avg(salary)

FROM employeesGROUP BY department_id

SELECT DISTINCT department_idFROM employees

Page 7: PGDay UK 2016 -- Performace for queries with grouping

7/50

Examples

SELECT DISTINCT ON (department_id)department_id,employee_id,salary

FROM employeesORDER BY department_id,

salary DESC

Page 8: PGDay UK 2016 -- Performace for queries with grouping

8/50

Examples

SELECT max(salary)FROM employees

SELECT salaryFROM employeesORDER BY salary DESCLIMIT 1

Page 9: PGDay UK 2016 -- Performace for queries with grouping

9/50

How does it work?

Page 10: PGDay UK 2016 -- Performace for queries with grouping

10/50

Aggregation functions under the hood

INITCOND SFUNC

Input data

state SFUNC

Input data

state SFUNC

Input data

state

FINALFUNC

Result

An aggregate function is defined by:State, input and output typesInitial state (INITCOND)Transition function (SFUNC)Final function (FINALFUNC)

SELECT sum(column1),avg(column1)

FROM (VALUES (2), (3), (7)) _

Page 11: PGDay UK 2016 -- Performace for queries with grouping

10/50

Aggregation functions under the hood

state = 0 state += input

2

2 state += input

3

5 state += input

7

12

=

sum=12

SELECT sum(column1),avg(column1)

FROM (VALUES (2), (3), (7)) _

Page 12: PGDay UK 2016 -- Performace for queries with grouping

10/50

Aggregation functions under the hood

cnt = 0sum = 0

cnt++sum+=input

2

cnt=1sum=2

cnt++sum+=input

3

cnt=2sum=5

cnt++sum+=input

7

cnt=3sum=12

sum / cnt

avg=4

SELECT sum(column1),avg(column1)

FROM (VALUES (2), (3), (7)) _

Page 13: PGDay UK 2016 -- Performace for queries with grouping

11/50

Aggregation functions under the hood

SFUNC and FINALFUNC functions can be written inC — fast (SFUNC may reuse state variable)SQLPL/pgSQL — SLOW!any other language

SFUNC and FINALFUNC functions can be declared STRICT(i.e. not called on null input)

Page 14: PGDay UK 2016 -- Performace for queries with grouping

12/50

Grouping algorithms

PostgreSQL uses 2 algorithms to feed aggregate functions bygrouped data:

GroupAggregate: get the data sorted and applyaggregation function to groups one by oneHashAggregate: store state for each key in a hash table

Page 15: PGDay UK 2016 -- Performace for queries with grouping

13/50

GroupAgg

1 3 1 2 2 3 1 3 2 1 state: 0

1 3 1 2 2 3 1 3 state: 3

1 3 1 2 2 state: 4 6

1 3 1 state: 0 8 6

5 8 6

Page 16: PGDay UK 2016 -- Performace for queries with grouping

13/50

GroupAgg

1 3 1 2 2 3 1 3 2 1 state: 0

1 3 1 2 2 3 1 3 state: 3

1 3 1 2 2 state: 4 6

1 3 1 state: 0 8 6

5 8 6

Page 17: PGDay UK 2016 -- Performace for queries with grouping

13/50

GroupAgg

1 3 1 2 2 3 1 3 2 1 state: 0

1 3 1 2 2 3 1 3 state: 3

1 3 1 2 2 state: 4 6

1 3 1 state: 0 8 6

5 8 6

Page 18: PGDay UK 2016 -- Performace for queries with grouping

13/50

GroupAgg

1 3 1 2 2 3 1 3 2 1 state: 0

1 3 1 2 2 3 1 3 state: 3

1 3 1 2 2 state: 4 6

1 3 1 state: 0 8 6

5 8 6

Page 19: PGDay UK 2016 -- Performace for queries with grouping

13/50

GroupAgg

1 3 1 2 2 3 1 3 2 1 state: 0

1 3 1 2 2 3 1 3 state: 3

1 3 1 2 2 state: 4 6

1 3 1 state: 0 8 6

5 8 6

Page 20: PGDay UK 2016 -- Performace for queries with grouping

14/50

HashAggregate

1 2 3 2 3 1 2 1 3 1 state: 0

1 2 3 2 3 1 2 1 3 state: 1

1 2 3 2 3 1 2 1state: 1

state: 3

1 2 3

state: 6

state: 6

state: 1

state: 6

state: 8state: 5

68 5

Page 21: PGDay UK 2016 -- Performace for queries with grouping

14/50

HashAggregate

1 2 3 2 3 1 2 1 3 1 state: 0

1 2 3 2 3 1 2 1 3 state: 1

1 2 3 2 3 1 2 1state: 1

state: 3

1 2 3

state: 6

state: 6

state: 1

state: 6

state: 8state: 5

68 5

Page 22: PGDay UK 2016 -- Performace for queries with grouping

14/50

HashAggregate

1 2 3 2 3 1 2 1 3 1 state: 0

1 2 3 2 3 1 2 1 3 state: 1

1 2 3 2 3 1 2 1state: 1

state: 3

1 2 3

state: 6

state: 6

state: 1

state: 6

state: 8state: 5

68 5

Page 23: PGDay UK 2016 -- Performace for queries with grouping

14/50

HashAggregate

1 2 3 2 3 1 2 1 3 1 state: 0

1 2 3 2 3 1 2 1 3 state: 1

1 2 3 2 3 1 2 1state: 1

state: 3

1 2 3

state: 6

state: 6

state: 1

state: 6

state: 8state: 5

68 5

Page 24: PGDay UK 2016 -- Performace for queries with grouping

14/50

HashAggregate

1 2 3 2 3 1 2 1 3 1 state: 0

1 2 3 2 3 1 2 1 3 state: 1

1 2 3 2 3 1 2 1state: 1

state: 3

1 2 3

state: 6

state: 6

state: 1

state: 6

state: 8state: 5

68 5

Page 25: PGDay UK 2016 -- Performace for queries with grouping

14/50

HashAggregate

1 2 3 2 3 1 2 1 3 1 state: 0

1 2 3 2 3 1 2 1 3 state: 1

1 2 3 2 3 1 2 1state: 1

state: 3

1 2 3

state: 6

state: 6

state: 1

state: 6

state: 8state: 5

68 5

Page 26: PGDay UK 2016 -- Performace for queries with grouping

15/50

GroupAggregate vs. HashAggregate

GroupAggregate− Requires sorted data+ Needs less memory+ Returns sorted data+ Returns data on the fly+ Can perform

count(distinct x),array_agg(x order by y)etc.

+ On cardinalitymisestimation will sort ondisk

HashAggregate+ Accepts unsorted data− Needs more memory− Returns unsorted data− Returns data at the end− Can perform only basic

aggregation− On groups count

misestimation will OOM

Page 27: PGDay UK 2016 -- Performace for queries with grouping

16/50

Optimisation: avoid sorts

Page 28: PGDay UK 2016 -- Performace for queries with grouping

17/50

Simple group-by: avoid sorts

Sorts are really slow. Prefer HashAggregation if possible.

100 101 102 103 104 105 106 1070

1

2

3

4

5

6

7

Groups

Tim

e,s

SELECT a, COUNT(*) FROM t_10m GROUP BY a

HashAggSort + GroupAgg

What to do if you get something like this?

EXPLAINSELECT region_id,

avg(age)FROM peopleGROUP BY region_id

Increase work_mem: set work_mem to ’100MB’HashAggregate (cost=20406.00..20530.61 rows=9969 width=10)

-> Seq Scan on people (cost=0.00..15406.00 rows=1000000 width=10)

685.689 msIncrease sanely to avoid OOM

Page 29: PGDay UK 2016 -- Performace for queries with grouping

17/50

Simple group-by: avoid sorts

Sorts are really slow. Prefer HashAggregation if possible.

What to do if you get something like this?

EXPLAINSELECT region_id,

avg(age)FROM peopleGROUP BY region_id

GroupAggregate (cost=149244.84..156869.46 rows=9969 width=10)-> Sort (cost=149244.84..151744.84 rows=1000000 width=10)

Sort Key: region_id-> Seq Scan on people (cost=0.00..15406.00 rows=1000000 width=10)

1504.474 ms

Increase work_mem: set work_mem to ’100MB’HashAggregate (cost=20406.00..20530.61 rows=9969 width=10)

-> Seq Scan on people (cost=0.00..15406.00 rows=1000000 width=10)

685.689 msIncrease sanely to avoid OOM

Page 30: PGDay UK 2016 -- Performace for queries with grouping

17/50

Simple group-by: avoid sorts

Sorts are really slow. Prefer HashAggregation if possible.

What to do if you get something like this?

EXPLAINSELECT region_id,

avg(age)FROM peopleGROUP BY region_id

set enable_sort to off?

No!GroupAggregate (cost=10000149244.84..10000156869.46 rows=9969 width=10)

-> Sort (cost=10000149244.84..10000151744.84 rows=1000000 width=10)Sort Key: region_id-> Seq Scan on people (cost=0.00..15406.00 rows=1000000 width=10)

1497.167 msIncrease work_mem: set work_mem to ’100MB’HashAggregate (cost=20406.00..20530.61 rows=9969 width=10)

-> Seq Scan on people (cost=0.00..15406.00 rows=1000000 width=10)

685.689 msIncrease sanely to avoid OOM

Page 31: PGDay UK 2016 -- Performace for queries with grouping

17/50

Simple group-by: avoid sorts

Sorts are really slow. Prefer HashAggregation if possible.

What to do if you get something like this?

EXPLAINSELECT region_id,

avg(age)FROM peopleGROUP BY region_id

set enable_sort to off? No!GroupAggregate (cost=10000149244.84..10000156869.46 rows=9969 width=10)

-> Sort (cost=10000149244.84..10000151744.84 rows=1000000 width=10)Sort Key: region_id-> Seq Scan on people (cost=0.00..15406.00 rows=1000000 width=10)

1497.167 ms

Increase work_mem: set work_mem to ’100MB’HashAggregate (cost=20406.00..20530.61 rows=9969 width=10)

-> Seq Scan on people (cost=0.00..15406.00 rows=1000000 width=10)

685.689 msIncrease sanely to avoid OOM

Page 32: PGDay UK 2016 -- Performace for queries with grouping

17/50

Simple group-by: avoid sorts

Sorts are really slow. Prefer HashAggregation if possible.

What to do if you get something like this?

EXPLAINSELECT region_id,

avg(age)FROM peopleGROUP BY region_id

Increase work_mem: set work_mem to ’100MB’

HashAggregate (cost=20406.00..20530.61 rows=9969 width=10)-> Seq Scan on people (cost=0.00..15406.00 rows=1000000 width=10)

685.689 ms

Increase sanely to avoid OOM

Page 33: PGDay UK 2016 -- Performace for queries with grouping

17/50

Simple group-by: avoid sorts

Sorts are really slow. Prefer HashAggregation if possible.

What to do if you get something like this?

EXPLAINSELECT region_id,

avg(age)FROM peopleGROUP BY region_id

Increase work_mem: set work_mem to ’100MB’

HashAggregate (cost=20406.00..20530.61 rows=9969 width=10)-> Seq Scan on people (cost=0.00..15406.00 rows=1000000 width=10)

685.689 msIncrease sanely to avoid OOM

Page 34: PGDay UK 2016 -- Performace for queries with grouping

18/50

Simple group-by: avoid sorts

How to spend less memory to allow HashAggregation?

Don’t aggregate joinedSELECT p.region_id,

d.region_description,avg(age)

FROM people pJOIN regions r using (region_id)GROUP BY region_id,

region_description

Join aggregated insteadSELECT a.region_id,

r.region_description,a.avg_age

FROM (SELECT region_id,

avg(age) avg_ageFROM people pGROUP BY region_id

) aJOIN regions r using (region_id)

Page 35: PGDay UK 2016 -- Performace for queries with grouping

19/50

Count distinct: avoid sorts as well

How to avoid sorts for count(DISTINCT ...)?

SELECT location_id,count(DISTINCT visitor_id)

FROM visitsGROUP BY location_id

GroupAggregate (actual time=2371.992..4832.437 rows=1000 loops=1)Group Key: location_id-> Sort (actual time=2369.322..3488.261 rows=10000000 loops=1)

Sort Key: location_idSort Method: quicksort Memory: 818276kB-> Seq Scan on visitors (actual time=0.007..943.090 rows=10000000 loops=1)

Page 36: PGDay UK 2016 -- Performace for queries with grouping

20/50

Count distinct: avoid sorts as well!

Two levels of HashAggregate could be faster!

SELECT location_id,count(*)

FROM (SELECT DISTINCT location_id,

visitor_idFROM visits

) _GROUP BY location_id

HashAggregate (actual time=2409.378..2409.471 rows=1000 loops=1)Group Key: visits.location_id-> HashAggregate (actual time=2235.069..2235.156 rows=1000 loops=1)

Group Key: visits.location_id, visits.visitor_id-> Seq Scan on visits (actual time=0.005..884.194 rows=10000000 loops=1)

Page 37: PGDay UK 2016 -- Performace for queries with grouping

21/50

Count distinct: avoid sorts as well!

Or use an extension by Tomáš Vondra:https://github.com/tvondra/count_distinct

SELECT location_id,count_distinct(visitor_id)

FROM visitsGROUP BY location_id

HashAggregate (actual time=3041.093..3041.466 rows=1000 loops=1)Group Key: visitor_id-> Seq Scan on visits (actual time=0.004..546.042 rows=10000000 loops=1)

Warning: this algorithm uses much memory in certaincircumstances

Page 38: PGDay UK 2016 -- Performace for queries with grouping

22/50

Count distinct: avoid sorts as well!

There is another extension that allows to calculate approximatenumber of distinct values using constant amount of memory:https://github.com/aggregateknowledge/postgresql-hll

SELECT location_id,hll_cardinality(

hll_add_agg(hll_hash_integer(c)))

FROM visitsGROUP BY location_id

HashAggregate (actual time=3848.346..3867.570 rows=1000 loops=1)Group Key: visitor_id-> Seq Scan on visits (actual time=0.004..546.042 rows=10000000 loops=1)

Page 39: PGDay UK 2016 -- Performace for queries with grouping

23/50

Count distinct: avoid sorts as well!

100 101 102 103 104012345678

Distinct values per group

Tim

e,s

Count-distinct from a 10M-rows table by 1000 groups

Sort+GroupAggHashAgg+HashAggCount_distinct ext.Postgres_hll ext.

Page 40: PGDay UK 2016 -- Performace for queries with grouping

24/50

Ordered aggregates: avoid massive sorts

How to avoid sorts for array_agg(...ORDER BY ...)?

SELECTvisit_date,array_agg(visitor_id ORDER BY visitor_id)

FROM visitsGROUP BY visit_date

GroupAggregate (actual time=5433.658..8010.309 rows=10000 loops=1)-> Sort (actual time=5433.416..6769.872 rows=4999067 loops=1)

Sort Key: visit_dateSort Method: external merge Disk: 107504kB-> Seq Scan on visits (actual time=0.046..581.672 rows=4999067 loops=1)

Page 41: PGDay UK 2016 -- Performace for queries with grouping

25/50

Avoiding sorts

Might be better to sort each line separately

SELECTvisit_date,(

select array_agg(i ORDER BY i)from unnest(visitors_u) i

)FROM (

SELECT visit_date,array_agg(visitor_id) visitors_u

FROM visitsGROUP BY visit_date

) _

Subquery Scan on _ (actual time=2504.915..3767.300 rows=10000 loops=1)-> HashAggregate (actual time=2504.757..2555.038 rows=10000 loops=1)

-> Seq Scan on visits (actual time=0.056..397.859 rows=4999067 loops=1)SubPlan 1

-> Aggregate (actual time=0.120..0.121 rows=1 loops=10000)-> Function Scan on unnest i (actual time=0.033..0.055 rows=500 loops=10000)

Page 42: PGDay UK 2016 -- Performace for queries with grouping

26/50

Summation optimisation

Page 43: PGDay UK 2016 -- Performace for queries with grouping

27/50

Summation: integer data types

smallint int bigint numeric

0

10

20

11 11

2320

12 12 12

21

Tim

e,s

Summating 100M numbers

9.49.5

sum(bigint) returns numeric was slow in 9.4 as it usedto convert every input value to numeric.

Page 44: PGDay UK 2016 -- Performace for queries with grouping

28/50

Summation: zeroes

0 % 20 % 40 % 60 % 80 % 100 %0

1

2

Non-zero values

Tim

e,s

Summation of 10M numerics

SELECT SUM(a) FROM tSELECT SUM(a) FROM t WHERE a <> 0SELECT SUM(a) FROM t, nulls stored instead of zeroes

Page 45: PGDay UK 2016 -- Performace for queries with grouping

29/50

Denormalized data aggregation

Page 46: PGDay UK 2016 -- Performace for queries with grouping

30/50

Denormalized data aggregation

Sometimes we need to aggregate denormalized data

Most common solution is

SELECT account_id,account_name,sum(payment_amount)

FROM paymentsGROUP BY account_id,

account_name

Planner does not know that account_id and account_namecorrelate. It can lead to wrong estimates and suboptimal plan.

Page 47: PGDay UK 2016 -- Performace for queries with grouping

31/50

Denormalized data aggregation

A bit less-known approach is

SELECT account_id,min(account_name),sum(payment_amount)

FROM paymentsGROUP BY account_id

Works only if the type of "denormalized payload" supportscomparison operator.

Page 48: PGDay UK 2016 -- Performace for queries with grouping

32/50

Denormalized data aggregation

Also we can write a custom aggregate function

CREATE FUNCTION frst (text, text)RETURNS text IMMUTABLE LANGUAGE sql AS

$$ select $1; $$;

CREATE AGGREGATE a (text) (SFUNC=frst,STYPE=text

);

SELECT account_id,a(account_name),sum(payment_amount)

FROM paymentsGROUP BY account_id

Page 49: PGDay UK 2016 -- Performace for queries with grouping

33/50

Denormalized data aggregation

Or even write it in C:https://github.com/bashtanov/argm

SELECT account_id,anyold(account_name),sum(payment_amount)

FROM paymentsGROUP BY account_id

Page 50: PGDay UK 2016 -- Performace for queries with grouping

34/50

Denormalized data aggregation

And what is the fastest?

It depends on the width of "denormalized payload":

1 10 100 1000 10000dumb 366ms 374ms 459ms 1238ms 53236ms

min 375ms 377ms 409ms 716ms 16747msSQL 1970ms 1975ms 2031ms 2446ms 2036ms

C 385ms 385ms 408ms 659ms 436ms

* — The more data the faster we proceed?It is because we do not need to extract TOASTed values.

Page 51: PGDay UK 2016 -- Performace for queries with grouping

34/50

Denormalized data aggregation

And what is the fastest?

It depends on the width of "denormalized payload":

1 10 100 1000 10000dumb 366ms 374ms 459ms 1238ms 53236ms

min 375ms 377ms 409ms 716ms 16747msSQL 1970ms 1975ms 2031ms 2446ms 2036ms*

C 385ms 385ms 408ms 659ms 436ms*

* — The more data the faster we proceed?It is because we do not need to extract TOASTed values.

Page 52: PGDay UK 2016 -- Performace for queries with grouping

35/50

Arg-maximum

Page 53: PGDay UK 2016 -- Performace for queries with grouping

36/50

Arg-maximum

Max

Population of the largestcity in each countryDate of last tweet by eachauthorThe highest salary in eachdepartment

Arg-maxWhat is the largest city ineach countryWhat is the last tweet byeach authorWho gets the highestsalary in each department

Page 54: PGDay UK 2016 -- Performace for queries with grouping

36/50

Arg-maximum

Max

Population of the largestcity in each countryDate of last tweet by eachauthorThe highest salary in eachdepartment

Arg-maxWhat is the largest city ineach countryWhat is the last tweet byeach authorWho gets the highestsalary in each department

Page 55: PGDay UK 2016 -- Performace for queries with grouping

37/50

Arg-maximum

Max is built-in. How to perform Arg-max?Self-joins?Window-functions?

Use DISTINCT ON() (PG-specific, not in SQL standard)

SELECT DISTINCT ON (author_id)author_id,twit_id

FROM twitsORDER BY author_id,

twit_date DESC

But it still can be performed only by sorting, not by hashing :(

Page 56: PGDay UK 2016 -- Performace for queries with grouping

37/50

Arg-maximum

Max is built-in. How to perform Arg-max?Self-joins?Window-functions?Use DISTINCT ON() (PG-specific, not in SQL standard)

SELECT DISTINCT ON (author_id)author_id,twit_id

FROM twitsORDER BY author_id,

twit_date DESC

But it still can be performed only by sorting, not by hashing :(

Page 57: PGDay UK 2016 -- Performace for queries with grouping

37/50

Arg-maximum

Max is built-in. How to perform Arg-max?Self-joins?Window-functions?Use DISTINCT ON() (PG-specific, not in SQL standard)

SELECT DISTINCT ON (author_id)author_id,twit_id

FROM twitsORDER BY author_id,

twit_date DESC

But it still can be performed only by sorting, not by hashing :(

Page 58: PGDay UK 2016 -- Performace for queries with grouping

38/50

Arg-maximum

We can emulate Arg-max by ordinary max and dirty hacks

SELECT author_id,(max(array[

twit_date,date’epoch’ + twit_id

]))[2] - date’epoch’FROM twitsGROUP BY author_id;

But such types tweaking is not always possible.

Page 59: PGDay UK 2016 -- Performace for queries with grouping

39/50

Arg-maximum

It’s time to write more custom aggregate functionsCREATE TYPE amax_ty AS (key_date date, payload int);

CREATE FUNCTION amax_t (p_state amax_ty, p_key_date date, p_payload int)RETURNS amax_ty IMMUTABLE LANGUAGE sql AS

$$SELECT CASE WHEN p_state.key_date < p_key_date

OR (p_key_date IS NOT NULL AND p_state.key_date IS NULL)THEN (p_key_date, p_payload)::amax_tyELSE p_state END

$$;

CREATE FUNCTION amax_f (p_state amax_ty) RETURNS int IMMUTABLE LANGUAGE sql AS$$ SELECT p_state.payload $$;

CREATE AGGREGATE amax (date, int) (SFUNC = amax_t,STYPE = amax_ty,FINALFUNC = amax_f,INITCOND = ’(,)’

);

SELECT author_id,amax(twit_date, twit_id)

FROM twitsGROUP BY author_id;

Page 60: PGDay UK 2016 -- Performace for queries with grouping

40/50

Arg-maximum

Argmax is similar to amax, but written in Chttps://github.com/bashtanov/argm

SELECT author_id,argmax(twit_date, twit_id)

FROM twitsGROUP BY author_id;

Page 61: PGDay UK 2016 -- Performace for queries with grouping

41/50

Arg-maximum

Who wins now?

1002 3332 10002 33332 50002

DISTINCT ON 6ms 42ms 342ms 10555ms 30421msMax(array) 5ms 47ms 399ms 4464ms 10025msSQL amax 38ms 393ms 3541ms 39539ms 90164msC argmax 5ms 37ms 288ms 3183ms 7176ms

SQL amax finally outperforms DISTINCT ON on 109-ish rows

Page 62: PGDay UK 2016 -- Performace for queries with grouping

41/50

Arg-maximum

Who wins now?

1002 3332 10002 33332 50002

DISTINCT ON 6ms 42ms 342ms 10555ms 30421msMax(array) 5ms 47ms 399ms 4464ms 10025msSQL amax 38ms 393ms 3541ms 39539ms 90164msC argmax 5ms 37ms 288ms 3183ms 7176ms

SQL amax finally outperforms DISTINCT ON on 109-ish rows

Page 63: PGDay UK 2016 -- Performace for queries with grouping

42/50

Some magic: loose index scan

Page 64: PGDay UK 2016 -- Performace for queries with grouping

43/50

Loose index scan

Slow distinct, max or arg-max query?Sometimes we can fetch the rows one-by-one using index:

3 2 1 4 2 2 1 3 31 0

CREATE TABLE balls(colour_id int, label int);INSERT INTO balls ...CREATE INDEX ON balls(colour_id);

-- find the very first colourSELECT colour_id FROM ballsORDER BY colour_id LIMIT 1;

-- find the next colourSELECT colour_id FROM ballsWHERE colour_id > ?ORDER BY colour_id LIMIT 1;-- and so on ...

Page 65: PGDay UK 2016 -- Performace for queries with grouping

44/50

Loose index scan

CREATE FUNCTION loosescan() RETURNSTABLE (o_colour_id int) AS $$

BEGINo_colour_id := -1; --less than all real idsLOOP

SELECT colour_idINTO o_colour_idFROM ballsWHERE colour_id > o_colour_idORDER BY colour_id LIMIT 1;

EXIT WHEN NOT FOUND; RETURN NEXT;END LOOP;

END;$$ LANGUAGE plpgsql;

SELECT * FROM loosescan();

Page 66: PGDay UK 2016 -- Performace for queries with grouping

45/50

Loose index scan

Or better do it in pure SQL instead

WITH RECURSIVE d AS ((

SELECT colour_idFROM ballsORDER BY colour_id LIMIT 1

)UNION

SELECT (SELECT b.colour_idFROM balls bWHERE b.colour_id > d.colour_idORDER BY b.colour_id LIMIT 1

) colour_idFROM d

)SELECT * FROM d WHERE colour_id IS NOT NULL;

Page 67: PGDay UK 2016 -- Performace for queries with grouping

46/50

Loose index scan

One-by-one retrieval by index+ Incredibly fast unless returns too many rows− Needs an index

Fetching distinct values from a 10M-rows table:101 103 105 106 107

HashAgg 1339ms 1377ms 2945ms 4086ms 5130msLIS proc 0ms 9ms 815ms 8004ms 80800msLIS SQL 0ms 6ms 555ms 5460ms 56153ms

Page 68: PGDay UK 2016 -- Performace for queries with grouping

47/50

Loose index scan

It is possible to explore similar approach for max and argmax+ Incredibly fast unless returns too many rows− Needs an index− SQL version needs tricks if the data types differ

1002 3332 10002 33332 50002

DISTINCT ON 6ms 42ms 342ms 10555ms 30421msMax(array) 5ms 47ms 399ms 4464ms 10025msSQL amax 38ms 393ms 3541ms 39539ms 90164msC argmax 5ms 37ms 288ms 3183ms 7176ms

LIS proc 2ms 6ms 12ms 42ms 63msLIS SQL 1ms 4ms 11ms 29ms 37ms

Page 69: PGDay UK 2016 -- Performace for queries with grouping

48/50

The future: parallel execution

Page 70: PGDay UK 2016 -- Performace for queries with grouping

49/50

The future: parallel execution

PostgreSQL 9.6 (currently Beta 2) introduces parallel executionof many nodes including aggregation.

Parallel aggregation extension is already available:http://www.cybertec.at/en/products/agg-parallel-aggregations-postgresql/

+ Up to 30 times faster+ Speeds up SeqScan as well− Mostly useful for complex row operations− Requires PG 9.5+

Page 71: PGDay UK 2016 -- Performace for queries with grouping

50/50

Questions?