请选择 进入手机版 | 继续访问电脑版

技术控

    今日:103| 主题:53936
收藏本版 (1)
最新软件应用技术尽在掌握

[其他] Faster PostgreSQL Counting

[复制链接]
近日我浮躁 发表于 2016-10-13 01:48:06
210 4

立即注册CoLaBug.com会员,免费获得投稿人的专业资料,享用更多功能,玩转个人品牌!

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
Everybody counts, but not always quickly. This article is a close look into how PostgreSQL optimizes counting. If you know the tricks there are ways to count rows orders of magnitude faster than you do already.
  The problem is actually underdescribed – there are several variations of counting, each with its own methods. First think whether you need an exact count or whether an estimate suffices. Next, are you counting duplicates or just distinct values? Finally do you want a lump count of an entire table or will you want to count only those rose matching extra criteria?
  We’ll analyze the techniques available for each situation and compare their speed and resource consumption. After learning about techniques for a single database we’ll use Citus to demonstrate how to parallelize counts in a distributed database.
  Table of Contents

  
       
  • Preparing the DB for tests   
  • Counts With Duplicates
           

      •       
      • Filtered Table Estimates      
           
       
  • Distinct Counts (No Duplicates)   
  • Reference of Techniques  
  Preparing the DB for tests

  The sections below use the following table for benchmarks.
  1. -- create a million random numbers and strings
  2. CREATE TABLE items AS
  3.   SELECT
  4.     (random()*1000000)::integer AS n,
  5.     md5(random()::text) AS s
  6.   FROM
  7.     generate_series(1,1000000);
  8. -- inform planner of big table size change
  9. VACUUM ANALYZE;
复制代码
Counts With Duplicates

  Exact Counts

   Let’s begin at the beginning, exact counts allowing duplication over some or all of a table, good old count(*) . Measuring the time to run this command provides a basis for evaluating the speed of other types of counting.
  Pgbench provides a convenient way to run a query repeatedly and collect statistics about performance.
  1. # Tests in this article were run against PostgreSQL 9.5.4
  2. echo "SELECT count(*) FROM items;" | pgbench -d count -t 50 -P 1 -f -
  3. # average  84.915 ms
  4. # stddev    5.251 ms
复制代码
  A note about count(1) vs count(*) . One might think that count(1) would be faster because count(*) appears to consult the data for a whole row. However the opposite is true. The star symbol is meaningless here, unlike its use in SELECT * . PostgreSQL parses The expression count(*) as a special case taking no arguments. (Historically the expression ought to have been defined as count() .) On the other hand count(1) takes an argument and PostgreSQL has to check at every row to see that ts argument, 1, is indeed still not NULL.
   Running the above benchmark with count(1) results in:
  1. # average  98.896 ms
  2. # stddev    7.280 ms
复制代码
  However both both forms of count(1) and count(*) are fundamentally slow. PostgreSQL uses multiversion concurrency control (MVCC) to ensure consistency between simultaneous transactions. This means each transaction may see different rows – and different numbers of rows – in a table. There is no single universal row count that the database could cache, so it must scan through all rows counting how many are visible. Performance for an exact count grows linearly with table size.
  1. EXPLAIN SELECT count(*) FROM items;
  2. Aggregate  (cost=20834.00..20834.01 rows=1 width=0)
  3.   ->  Seq Scan on items  (cost=0.00..18334.00 rows=1000000 width=0)
复制代码
The scan accounts for 88% of the total cost. As we double the table size the query time roughly doubles, with cost of scanning and aggregating growing proportionally with one other.
              Rows     Avg Time                   1M     85ms             2M     161ms             4M     343ms           How can we make this faster? Something has to give, either we can settle for an estimated rather than exact count, or we can cache the count ourselves using a manual increasing-decreasing tally. However in the second case we have to keep a tally for each table and where clause that we want to count quickly later.
   Here’s an example of the tally approach applied to the whole items table. The following trigger-based solution is adapted from A. Elein Mustain . PostgreSQL’s MVCC will maintain consistency between the items table and a table of row counts.
  1. BEGIN;
  2. CREATE TABLE row_counts (
  3.   relname   text PRIMARY KEY,
  4.   reltuples bigint
  5. );
  6. -- establish initial count
  7. INSERT INTO row_counts (relname, reltuples)
  8.   VALUES ('items', (SELECT count(*) from items));
  9. CREATE OR REPLACE FUNCTION adjust_count()
  10. RETURNS TRIGGER AS
  11. $$
  12.    DECLARE
  13.    BEGIN
  14.    IF TG_OP = 'INSERT' THEN
  15.       EXECUTE 'UPDATE row_counts set reltuples=reltuples +1 where relname = ''' || TG_RELNAME || '''';
  16.       RETURN NEW;
  17.    ELSIF TG_OP = 'DELETE' THEN
  18.       EXECUTE 'UPDATE row_counts set reltuples=reltuples -1 where relname = ''' || TG_RELNAME || '''';
  19.       RETURN OLD;
  20.    END IF;
  21.    END;
  22. $$
  23. LANGUAGE 'plpgsql';
  24. CREATE TRIGGER items_count BEFORE INSERT OR DELETE ON items
  25.   FOR EACH ROW EXECUTE PROCEDURE adjust_count();
  26. COMMIT;
复制代码
  The speed of reading and updating the cached value is independent of the table size, and reading is very fast. However this technique shifts overhead to inserts and deletes. Without the trigger the following statement takes an average of 4.7 seconds, whereas inserts with the trigger are fifty times slower :
  1. INSERT INTO items (n, s)
  2.   SELECT
  3.     (random()*1000000)::integer AS n,
  4.     md5(random()::text) AS s
  5.   FROM generate_series(1,1000000);
复制代码
Estimated Counts

  Full Table Estimates

   The previous “tally” approach for caching table counts makes inserts slow. If we’re willing to accept an estimated rather than exact count we can get fast reads like the tally but with no insert degradation. To do so we can lean on estimates gathered from PostgreSQL subsystems. Two sources are the stats collector and the autovacuum daemon .
  Here are two alternatives for getting the estimate:
  1. -- Asking the stats collector
  2. SELECT n_live_tup
  3.   FROM pg_stat_all_tables
  4. WHERE relname = 'items';
  5. -- Updated by VACUUM and ANALYZE
  6. SELECT reltuples
  7.   FROM pg_class
  8. WHERE relname = 'items';
复制代码
However there’s a more accurate source that is less likely to be stale. Andrew Gierth (RhodiumToad) advises:
  Remember that reltuples isn’t the estimate that the planner actually uses; the planner uses reltuples/relpages multiplied by the current number of pages.
  Here’s the intuition. As the sample of data in a table increases then the average number of rows fitting in a physical page is likely to change less drastically than number of rows total. We can multiply the average rows per page by up-to-date information about the current number of pages occupied by a table for a more accurate estimation of the current number of rows.
  1. -- pg_relation_size and block size have fresh info so combine them with
  2. -- the estimation of tuples per page
  3. SELECT
  4.   (reltuples/relpages) * (
  5.     pg_relation_size('items') /
  6.     (current_setting('block_size')::integer)
  7.   )
  8.   FROM pg_class where relname = 'items';
复制代码
Filtered Table Estimates

   The previous section gets estimated counts for entire tables, but is there a way to get one for only those rows matching a condition? Michael Fuhr came up with a clever trick to run EXPLAIN for a query and parse its output.
  1. CREATE FUNCTION count_estimate(query text) RETURNS integer AS $$
  2. DECLARE
  3.   rec   record;
  4.   rows  integer;
  5. BEGIN
  6.   FOR rec IN EXECUTE 'EXPLAIN ' || query LOOP
  7.     rows := substring(rec."QUERY PLAN" FROM ' rows=([[:digit:]]+)');
  8.     EXIT WHEN rows IS NOT NULL;
  9.   END LOOP;
  10.   RETURN rows;
  11. END;
  12. $$ LANGUAGE plpgsql VOLATILE STRICT;
复制代码
We can use the function like this:
  1. SELECT count_estimate('SELECT 1 FROM items WHERE n < 1000');
复制代码
  The accuracy of this method relies on the planner which uses several techniques to estimate the selectivity of a where clause and from there the number of rows that will be returned.
  Distinct Counts (No Duplicates)

  Exact Counts

  Default behavior under low memory

   Count with duplicates may be slow, but count distinct is much worse. With limited working memory and no indices, PostgreSQL is unable to optimize much. In its stock configuration PostgreSQL specifies a low memory limit per concurrent query ( work_mem ). On my development machine the default was four megabytes.
  Sticking with default, here is the performance for dealing with a million rows.
  1. # Tests in this article were run against PostgreSQL 9.5.4
  2. echo "SELECT count(*) FROM items;" | pgbench -d count -t 50 -P 1 -f -
  3. # average  84.915 ms
  4. # stddev    5.251 ms0
复制代码
Running EXPLAIN shows that the bulk of the query happens in the aggregate, and that running the count on a string column takes longer than the integer column:
   

Faster PostgreSQL Counting

Faster PostgreSQL Counting

  1. # Tests in this article were run against PostgreSQL 9.5.4
  2. echo "SELECT count(*) FROM items;" | pgbench -d count -t 50 -P 1 -f -
  3. # average  84.915 ms
  4. # stddev    5.251 ms1
复制代码
  What is happening inside the aggregate though? Its description in the explain output is opaque. We can get an idea by inquiring about a related query, select distinct rather than count distinct .
   

Faster PostgreSQL Counting

Faster PostgreSQL Counting

  1. # Tests in this article were run against PostgreSQL 9.5.4
  2. echo "SELECT count(*) FROM items;" | pgbench -d count -t 50 -P 1 -f -
  3. # average  84.915 ms
  4. # stddev    5.251 ms2
复制代码
  Without more work_mem or external data structures like an index PostgreSQL merge-sorts the table between memory and disk and then iterates through the results removing duplicates, much like the classic Unix combination sort | uniq .
   Sorting takes most the query time, especially if we select the string column s rather than the integer column n . In both cases the unique filter goes at about the same speed.
  Custom aggregate

  Thomas Vondra created a custom aggregate for counting distinct values in columns of fixed-width types (additionally the types must have at most 64 bits). Without any extra work memory or indices it beats the default sort-based counting. To install
  
       
  • Clone the project, tvondra/count_distinct   
  • Run make install   
  • In your database: CREATE EXTENSION count_distinct;  
   Thomas explains how the aggregate works in this blog post but the short description is that it builds a sorted array of unique elements in memory, compacting as it goes.
  1. # Tests in this article were run against PostgreSQL 9.5.4
  2. echo "SELECT count(*) FROM items;" | pgbench -d count -t 50 -P 1 -f -
  3. # average  84.915 ms
  4. # stddev    5.251 ms3
复制代码
This beats the standard count distinct aggregate which took an average of 742 ms for our dataset. Note that custom extensions written in C like count_distinct are not bound by the value of work_mem. The array constructed in this extension can exceed your memory expectations.
  HashAggregate

  When all rows to be counted can fit in work_mem then PostgreSQL uses a hash table to get distinct values:
   

Faster PostgreSQL Counting

Faster PostgreSQL Counting

  1. # Tests in this article were run against PostgreSQL 9.5.4
  2. echo "SELECT count(*) FROM items;" | pgbench -d count -t 50 -P 1 -f -
  3. # average  84.915 ms
  4. # stddev    5.251 ms4
复制代码
  This is the fastest way discussed thus far to get distinct values. It takes an average of 372 ms for n and 23 seconds for s . The queries select distinct n and select count(distinct n) take roughly the same amount of time to run, suggesting that the count distinct aggregate is using a HashAggregate inside as well.
   Be careful, setting a high enough memory allowance to activate this method may not be desirable. Remember that work_mem applies for all concurrent queries individually, so it can add up. Besides, we can do better.
  Index-Only Scan

  PostgreSQL 9.2 introduced this performance feature. When an index contains all information required by a query, the database can walk through the index alone without touching any of the regular table storage (“the heap”). The index type must support index-only scans. Btree indexes always do. GiST and SP-GiST indexes support index-only scans for some operator classes but not others.
   We’ll use btree indices and will create one each for the n and s columns:
  1. # Tests in this article were run against PostgreSQL 9.5.4
  2. echo "SELECT count(*) FROM items;" | pgbench -d count -t 50 -P 1 -f -
  3. # average  84.915 ms
  4. # stddev    5.251 ms5
复制代码
Selecting distinct values of these columns now uses a new strategy:
   

Faster PostgreSQL Counting

Faster PostgreSQL Counting

  1. # Tests in this article were run against PostgreSQL 9.5.4
  2. echo "SELECT count(*) FROM items;" | pgbench -d count -t 50 -P 1 -f -
  3. # average  84.915 ms
  4. # stddev    5.251 ms6
复制代码
  But now we come to a quirk, SELECT COUNT(DISTINCT n) FROM items will not use the index even though SELECT DISTINCT n does. As many blog posts mention ( “one weird trick to make postgres 50x faster!” ) you can guide the planner by rewriting count distinct as the count of a subquery:
  1. # Tests in this article were run against PostgreSQL 9.5.4
  2. echo "SELECT count(*) FROM items;" | pgbench -d count -t 50 -P 1 -f -
  3. # average  84.915 ms
  4. # stddev    5.251 ms7
复制代码
  An in-order binary tree traversal is fast. This query takes an average of 177 ms (or 270 ms for the s column).
   A word of warning. When work_mem is high enough to hold the whole relation PostgreSQL will choose HashAggregate even when an index exists. Paradoxically, giving the database more memory resources can lead to a worse plan. You can force the index-only scan by setting SET enable_hashagg=false; but remember to set it true again afterward or other query plans will get messed up.
  Estimated Counts

  HyperLogLog

  The previous techniques rely on either fitting an index, hash-table or sorted array in memory, or else consulting the statistics tables from a single database instance. When data grows truly large and/or spreads between multiple database instances these techniques begin to break down.
  Probabilistic data structures help here, they give fast approximate answers and easily parallelize. We’ll use one for querying distinct counts, a cardinality estimator called HyperLogLog (HLL). It uses a small amount of memory to represent a set of items. Importantly for us its union operation is lossless, meaning taking the union of arbitrary HLL values does not degrade the precision of their estimation.
  The intuitive idea behind HLL is rooted in the behavior of a good hash function, particularly the distance between hashed values. A function which distributes items evenly tends to keep them spread apart. As more items are hashed they begin to run out of room and crowd closer together. By keeping track of some of the smallest distances between hashed values the algorithm can estimate the most likely number of hashed inputs that caused the crowding.
  Let’s measure the speed. First install the PostgreSQL extension.
  
       
  • Clone postgresql-hll   
  • Run make install   
  • In your database: CREATE EXTENSION hll;  
  The way it plays out is by providing a fast aggregate that acts on a sequential scan:
  1. # Tests in this article were run against PostgreSQL 9.5.4
  2. echo "SELECT count(*) FROM items;" | pgbench -d count -t 50 -P 1 -f -
  3. # average  84.915 ms
  4. # stddev    5.251 ms8
复制代码
  The average speed of HLL count distinct on n is 239 ms, and 284 ms on s . Thus it is slightly slower than an index-only scan for data of this size. Its true strength comes from the fact that HLL unions are lossless, associative and commutative. This means they can be parallelized across servers and combined for a final result.
  Parallelization

   Applications that conduct real-time analytics, such as google analytics, use counts extensively and counting is an operation which can parallelize well. This section will measure the performance of a few techniques in a small Citus cluster running onCitus Cloud.
  The general idea is to run separate database instances (workers) across multiple machines. The instances share a schema and each instance holds portions (shards), of the total dataset. Workers can count rows in parallel.
  Setting up a Cluster

  For our example we will create a small cluster, as our purpose is to compare the performance improvements of several techniques rather than strive for ultimate benchmarking speed.
   For this example I created an eight-machine cluster on Citus Cloud and selected the smallest allowable hardware configuration for each worker. If you would like to try this example yourself you cansign up for an account.
  After creating a cluster I connect to the coordinator node to run the SQL. First create a table as before.
  1. # Tests in this article were run against PostgreSQL 9.5.4
  2. echo "SELECT count(*) FROM items;" | pgbench -d count -t 50 -P 1 -f -
  3. # average  84.915 ms
  4. # stddev    5.251 ms9
复制代码
At this point the table exists only in the coordinator database. We need to shard the table across the workers and then populate the shards with rows. Citus assigns each row to a unique shard by looking at the values in our choice of “distribution column.” Below we tell it to distribute future rows in the items table by hashing their n column to a shard assignment.
  1. # average  98.896 ms
  2. # stddev    7.280 ms0
复制代码
  We’ll load random data into the shards through the coordinator node. (Citus also supportsMX, a “masterless” mode for faster data loading but we don’t need it for this example).
  After obtaining the cluster coordinator database URL, run the following on a computer with fast network access. (All the generated data passes through this computer, hence the need for good network speed.)
  1. # average  98.896 ms
  2. # stddev    7.280 ms1
复制代码
Whereas we used a million rows in the single-database tests, this time we turned it up to a hundred million.
  Exact Counts

  Non-Distinct

  Plain counts (non-distinct) have no problems. The coordinator runs the query on all workers and then adds up the results. The output of EXPLAIN shows the plan chosen on a representative worker (“Distributed Query”) and the plan used on the coordinator (“Master Query”).
  1. # average  98.896 ms
  2. # stddev    7.280 ms2
复制代码
For reference the count query completes in an average of 1.2 seconds on this cluster. Distinct counts pose a more difficult problem across shards.
  Distinct

  The difficulty with counting distinct values of a column across shards is that items may be duplicated between shards and thus double-counted. However this is not a problem when counting values in the distribution column. Any two rows with the same value in this column will have been hashed to the same shard placement and will avoid cross-shard duplication.
  For count distinct on the distribution column Citus knows to push the query down to each worker, then sum the results. On our example cluster it completes in an average of 3.4 seconds.
  The more difficult case is doing a distinct count of a non-distribution column. Logically there are two possibilities:
  
       
  • Pull all rows to the coordinator and count there.   
  • Reshuffle rows between workers to avoid duplication of column values between workers, then do a distinct count on each worker and sum the results on the coordinator.  
  The first option is really no better than counting on a single database instance – in fact it is exactly that, plus high network overhead. The second option is the way to go.
  The second option is called “repartitioning.” The idea is to make temporary tables on the workers using a new distribution column. Workers send rows amongst each other to populate the temp tables, perform the query, and remove the tables. Different distributed databases perform repartitions under different query conditions. The specific details of repartition queries in Citus are beyond the scope of this article.
  Estimated Count Distinct

  Cardinality estimators like HLL are a lifesaver in distributed databases. They allow the system to make distinct counts for even non-distribution columns with little network overhead: the HLL datatype has a small byte size and can be sent quickly from workers to the coordinator. Because its union operation is lossless we needn’t worry about the number of workers affecting its precision.
   On Citus in particular you don’t need to explicitly invoke postgresql-hll functions. Simply make citus.count_distinct_error_rate non-zero and Citus will rewrite count distinct query plans to use HLL. For instance:
  1. # average  98.896 ms
  2. # stddev    7.280 ms3
复制代码
It’s fast too: 3.2 seconds for counting distinct values of n, and 3.8 seconds for s across 100 million records, on a non-distribution column and a string to boot. HLL is a great way to horizontally scale distinct count estimates in a distributed database.
  Reference of Techniques

              Technique     Time/1M rows     Exact     Filterable     Distinct                   PG Stats     0.3ms     :x:     :x:     :x:          Parse EXPLAIN   0.3ms   :x:   :white_check_mark:   :x:   Tally   2ms (but slow inserts)   :white_check_mark:   :x:   :x:   count(*)   85ms   :white_check_mark:   :white_check_mark:   :x:       count(1)    99ms    :white_check_mark:    :white_check_mark:    :x:          Idx Only Scan    177ms    :white_check_mark:    :white_check_mark:    :white_check_mark:          HLL    239ms    :x:    :white_check_mark:    :white_check_mark:          HashAgg    372ms    :white_check_mark:    :white_check_mark:    :white_check_mark:          Custom Agg    435ms (64-bit col)    :white_check_mark:    :white_check_mark:    :white_check_mark:          Mergesort    742ms    :white_check_mark:    :white_check_mark:    :white_check_mark:       HyperLogLog (HLL), although losing slightly to index-only scans in a single database on a table of one million rows, shines in larger tables (> 100GB). It is also particularly good in a distributed database, allowing real-time distinct count estimation over enormous datasets.
   We hope you found this guide on methods of counting in PostgreSQL helpful. If you have any questions about this post, or about how to effectively count over very large datasets please feel free to join us in ourslack channel to chat. And finally, if you did find it useful and want to hear more similar presentations about how things work in Postgres we encourage you to join us at PGConf Silicon Valley this November 14-16.



上一篇:Encrypt your –defaults-file
下一篇:Djinn, a theorem prover in Haskell, for Haskell (2005)
araw 发表于 2016-10-13 05:30:16
缺乏基情了!
回复 支持 反对

使用道具 举报

1q2 发表于 2016-10-15 18:30:55
楼下有什么好吐槽的么?
回复 支持 反对

使用道具 举报

就想爱你九年 发表于 2016-11-10 09:33:26
心里只有你一个频道,最可恨的是还没有广告。
回复 支持 反对

使用道具 举报

kukakia 发表于 2016-11-20 12:56:36
围观 围观 沙发在哪里!!!
回复 支持 反对

使用道具 举报

*滑动验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

我要投稿

推荐阅读

扫码访问 @iTTTTT瑞翔 的微博
回页顶回复上一篇下一篇回列表
手机版/CoLaBug.com ( 粤ICP备05003221号 | 文网文[2010]257号 )

© 2001-2017 Comsenz Inc. Design: Dean. DiscuzFans.

返回顶部 返回列表