Ignore BRIN indexes when checking for HOT udpates
authorTomas Vondra <[email protected]>
Tue, 30 Nov 2021 18:15:14 +0000 (19:15 +0100)
committerTomas Vondra <[email protected]>
Tue, 30 Nov 2021 19:04:38 +0000 (20:04 +0100)
When determining whether an index update may be skipped by using HOT, we
can ignore attributes indexed only by BRIN indexes. There are no index
pointers to individual tuples in BRIN, and the page range summary will
be updated anyway as it relies on visibility info.

This also removes rd_indexattr list, and replaces it with rd_attrsvalid
flag. The list was not used anywhere, and a simple flag is sufficient.

Patch by Josef Simanek, various fixes and improvements by me.

Author: Josef Simanek
Reviewed-by: Tomas Vondra, Alvaro Herrera
Discussion: https://postgr.es/m/CAFp7QwpMRGcDAQumN7onN9HjrJ3u4X3ZRXdGFT0K5G2JWvnbWg%40mail.gmail.com

15 files changed:
doc/src/sgml/indexam.sgml
src/backend/access/brin/brin.c
src/backend/access/gin/ginutil.c
src/backend/access/gist/gist.c
src/backend/access/hash/hash.c
src/backend/access/heap/heapam.c
src/backend/access/nbtree/nbtree.c
src/backend/access/spgist/spgutils.c
src/backend/utils/cache/relcache.c
src/include/access/amapi.h
src/include/utils/rel.h
src/include/utils/relcache.h
src/test/modules/dummy_index_am/dummy_index_am.c
src/test/regress/expected/brin.out
src/test/regress/sql/brin.sql

index cf359fa9ffd9701541ddbd8accd28120debb8af2..84de93107127f1818498b305c8ab9833d6217e7c 100644 (file)
@@ -126,6 +126,8 @@ typedef struct IndexAmRoutine
     bool        amcaninclude;
     /* does AM use maintenance_work_mem? */
     bool        amusemaintenanceworkmem;
+    /* does AM block HOT update? */
+    bool        amhotblocking;
     /* OR of parallel vacuum flags */
     uint8       amparallelvacuumoptions;
     /* type of data stored in index, or InvalidOid if variable */
@@ -246,6 +248,15 @@ typedef struct IndexAmRoutine
    null, independently of <structfield>amoptionalkey</structfield>.
   </para>
 
+  <para>
+   The <structfield>amhotblocking</structfield> flag indicates whether the
+   access method blocks <acronym>HOT</acronym> when an indexed attribute is
+   updated. Access methods without pointers to individual tuples (like
+   <acronym>BRIN</acronym>) may allow <acronym>HOT</acronym> even in this
+   case. This does not apply to attributes referenced in index predicates,
+   an update of such attribute always disables <acronym>HOT</acronym>.
+  </para>
+
  </sect1>
 
  <sect1 id="index-functions">
index ccc9fa0959a9c9140e2bac078e43ed976b7c2046..f521bb963567e07b8f685c668a2d9303e4b4e895 100644 (file)
@@ -108,6 +108,7 @@ brinhandler(PG_FUNCTION_ARGS)
        amroutine->amcanparallel = false;
        amroutine->amcaninclude = false;
        amroutine->amusemaintenanceworkmem = false;
+       amroutine->amhotblocking = false;
        amroutine->amparallelvacuumoptions =
                VACUUM_OPTION_PARALLEL_CLEANUP;
        amroutine->amkeytype = InvalidOid;
index 6d2d71be32b0cbcd405cc9afcd101c327d4be8e0..066cf3e11ab66309762a3ae40975dec3b5978470 100644 (file)
@@ -56,6 +56,7 @@ ginhandler(PG_FUNCTION_ARGS)
        amroutine->amcanparallel = false;
        amroutine->amcaninclude = false;
        amroutine->amusemaintenanceworkmem = true;
+       amroutine->amhotblocking = true;
        amroutine->amparallelvacuumoptions =
                VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_CLEANUP;
        amroutine->amkeytype = InvalidOid;
index 0683f42c2588357bf5e64bbaeb5ee4b2a86a32fd..d96ce1c0a99b2bdff21737b770103e516dc7329c 100644 (file)
@@ -77,6 +77,7 @@ gisthandler(PG_FUNCTION_ARGS)
        amroutine->amcanparallel = false;
        amroutine->amcaninclude = true;
        amroutine->amusemaintenanceworkmem = false;
+       amroutine->amhotblocking = true;
        amroutine->amparallelvacuumoptions =
                VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
        amroutine->amkeytype = InvalidOid;
index eb3810494f2f9a42d35be4ae27e9e9f49bee8b89..81c7da7ec693645828267d0d802061ce24496d2d 100644 (file)
@@ -74,6 +74,7 @@ hashhandler(PG_FUNCTION_ARGS)
        amroutine->amcanparallel = false;
        amroutine->amcaninclude = false;
        amroutine->amusemaintenanceworkmem = false;
+       amroutine->amhotblocking = true;
        amroutine->amparallelvacuumoptions =
                VACUUM_OPTION_PARALLEL_BULKDEL;
        amroutine->amkeytype = INT4OID;
index 29a4bf0c776c9d9d471092167c8af3799199d4a4..17afe1ea4c073ae60d59f80edd42e47e4480c968 100644 (file)
@@ -3223,7 +3223,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
         * Note that we get copies of each bitmap, so we need not worry about
         * relcache flush happening midway through.
         */
-       hot_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_ALL);
+       hot_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_HOT_BLOCKING);
        key_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_KEY);
        id_attrs = RelationGetIndexAttrBitmap(relation,
                                                                                  INDEX_ATTR_BITMAP_IDENTITY_KEY);
index 40ad0956e0048406aac5bb3c72a873e214849fe5..bd6d6b1cc9370de6ff3e09b84c3fb26020875328 100644 (file)
@@ -113,6 +113,7 @@ bthandler(PG_FUNCTION_ARGS)
        amroutine->amcanparallel = true;
        amroutine->amcaninclude = true;
        amroutine->amusemaintenanceworkmem = false;
+       amroutine->amhotblocking = true;
        amroutine->amparallelvacuumoptions =
                VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
        amroutine->amkeytype = InvalidOid;
index 3235d215e1a84ad05ea251a236f8b6173a1d3e5c..7760de94f3d714f378f86816bed3460a4c644ef8 100644 (file)
@@ -62,6 +62,7 @@ spghandler(PG_FUNCTION_ARGS)
        amroutine->amcanparallel = false;
        amroutine->amcaninclude = true;
        amroutine->amusemaintenanceworkmem = false;
+       amroutine->amhotblocking = true;
        amroutine->amparallelvacuumoptions =
                VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
        amroutine->amkeytype = InvalidOid;
index 9fa9e671a114e546b6ec3264d0142cc7b78e8c70..e1ea079e9e36348241cf94d197448ff9fbcdd79e 100644 (file)
@@ -2428,10 +2428,10 @@ RelationDestroyRelation(Relation relation, bool remember_tupdesc)
        list_free_deep(relation->rd_fkeylist);
        list_free(relation->rd_indexlist);
        list_free(relation->rd_statlist);
-       bms_free(relation->rd_indexattr);
        bms_free(relation->rd_keyattr);
        bms_free(relation->rd_pkattr);
        bms_free(relation->rd_idattr);
+       bms_free(relation->rd_hotblockingattr);
        if (relation->rd_pubactions)
                pfree(relation->rd_pubactions);
        if (relation->rd_options)
@@ -5105,10 +5105,10 @@ RelationGetIndexPredicate(Relation relation)
 Bitmapset *
 RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 {
-       Bitmapset  *indexattrs;         /* indexed columns */
        Bitmapset  *uindexattrs;        /* columns in unique indexes */
        Bitmapset  *pkindexattrs;       /* columns in the primary index */
        Bitmapset  *idindexattrs;       /* columns in the replica identity */
+       Bitmapset  *hotblockingattrs;   /* columns with HOT blocking indexes */
        List       *indexoidlist;
        List       *newindexoidlist;
        Oid                     relpkindex;
@@ -5117,18 +5117,18 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
        MemoryContext oldcxt;
 
        /* Quick exit if we already computed the result. */
-       if (relation->rd_indexattr != NULL)
+       if (relation->rd_attrsvalid)
        {
                switch (attrKind)
                {
-                       case INDEX_ATTR_BITMAP_ALL:
-                               return bms_copy(relation->rd_indexattr);
                        case INDEX_ATTR_BITMAP_KEY:
                                return bms_copy(relation->rd_keyattr);
                        case INDEX_ATTR_BITMAP_PRIMARY_KEY:
                                return bms_copy(relation->rd_pkattr);
                        case INDEX_ATTR_BITMAP_IDENTITY_KEY:
                                return bms_copy(relation->rd_idattr);
+                       case INDEX_ATTR_BITMAP_HOT_BLOCKING:
+                               return bms_copy(relation->rd_hotblockingattr);
                        default:
                                elog(ERROR, "unknown attrKind %u", attrKind);
                }
@@ -5159,7 +5159,7 @@ restart:
        relreplindex = relation->rd_replidindex;
 
        /*
-        * For each index, add referenced attributes to indexattrs.
+        * For each index, add referenced attributes to appropriate bitmaps.
         *
         * Note: we consider all indexes returned by RelationGetIndexList, even if
         * they are not indisready or indisvalid.  This is important because an
@@ -5168,10 +5168,10 @@ restart:
         * CONCURRENTLY is far enough along that we should ignore the index, it
         * won't be returned at all by RelationGetIndexList.
         */
-       indexattrs = NULL;
        uindexattrs = NULL;
        pkindexattrs = NULL;
        idindexattrs = NULL;
+       hotblockingattrs = NULL;
        foreach(l, indexoidlist)
        {
                Oid                     indexOid = lfirst_oid(l);
@@ -5236,8 +5236,9 @@ restart:
                         */
                        if (attrnum != 0)
                        {
-                               indexattrs = bms_add_member(indexattrs,
-                                                                                       attrnum - FirstLowInvalidHeapAttributeNumber);
+                               if (indexDesc->rd_indam->amhotblocking)
+                                       hotblockingattrs = bms_add_member(hotblockingattrs,
+                                                                                                attrnum - FirstLowInvalidHeapAttributeNumber);
 
                                if (isKey && i < indexDesc->rd_index->indnkeyatts)
                                        uindexattrs = bms_add_member(uindexattrs,
@@ -5254,10 +5255,15 @@ restart:
                }
 
                /* Collect all attributes used in expressions, too */
-               pull_varattnos(indexExpressions, 1, &indexattrs);
+               if (indexDesc->rd_indam->amhotblocking)
+                       pull_varattnos(indexExpressions, 1, &hotblockingattrs);
 
-               /* Collect all attributes in the index predicate, too */
-               pull_varattnos(indexPredicate, 1, &indexattrs);
+               /*
+                * Collect all attributes in the index predicate, too. We have to ignore
+                * amhotblocking flag, because the row might become indexable, in which
+                * case we have to add it to the index.
+                */
+               pull_varattnos(indexPredicate, 1, &hotblockingattrs);
 
                index_close(indexDesc, AccessShareLock);
        }
@@ -5285,25 +5291,25 @@ restart:
                bms_free(uindexattrs);
                bms_free(pkindexattrs);
                bms_free(idindexattrs);
-               bms_free(indexattrs);
+               bms_free(hotblockingattrs);
 
                goto restart;
        }
 
        /* Don't leak the old values of these bitmaps, if any */
-       bms_free(relation->rd_indexattr);
-       relation->rd_indexattr = NULL;
        bms_free(relation->rd_keyattr);
        relation->rd_keyattr = NULL;
        bms_free(relation->rd_pkattr);
        relation->rd_pkattr = NULL;
        bms_free(relation->rd_idattr);
        relation->rd_idattr = NULL;
+       bms_free(relation->rd_hotblockingattr);
+       relation->rd_hotblockingattr = NULL;
 
        /*
         * Now save copies of the bitmaps in the relcache entry.  We intentionally
-        * set rd_indexattr last, because that's the one that signals validity of
-        * the values; if we run out of memory before making that copy, we won't
+        * set rd_attrsvalid last, because that's what signals validity of the
+        * values; if we run out of memory before making that copy, we won't
         * leave the relcache entry looking like the other ones are valid but
         * empty.
         */
@@ -5311,20 +5317,21 @@ restart:
        relation->rd_keyattr = bms_copy(uindexattrs);
        relation->rd_pkattr = bms_copy(pkindexattrs);
        relation->rd_idattr = bms_copy(idindexattrs);
-       relation->rd_indexattr = bms_copy(indexattrs);
+       relation->rd_hotblockingattr = bms_copy(hotblockingattrs);
+       relation->rd_attrsvalid = true;
        MemoryContextSwitchTo(oldcxt);
 
        /* We return our original working copy for caller to play with */
        switch (attrKind)
        {
-               case INDEX_ATTR_BITMAP_ALL:
-                       return indexattrs;
                case INDEX_ATTR_BITMAP_KEY:
                        return uindexattrs;
                case INDEX_ATTR_BITMAP_PRIMARY_KEY:
                        return pkindexattrs;
                case INDEX_ATTR_BITMAP_IDENTITY_KEY:
                        return idindexattrs;
+               case INDEX_ATTR_BITMAP_HOT_BLOCKING:
+                       return hotblockingattrs;
                default:
                        elog(ERROR, "unknown attrKind %u", attrKind);
                        return NULL;
@@ -6180,10 +6187,11 @@ load_relcache_init_file(bool shared)
                rel->rd_indexlist = NIL;
                rel->rd_pkindex = InvalidOid;
                rel->rd_replidindex = InvalidOid;
-               rel->rd_indexattr = NULL;
+               rel->rd_attrsvalid = false;
                rel->rd_keyattr = NULL;
                rel->rd_pkattr = NULL;
                rel->rd_idattr = NULL;
+               rel->rd_hotblockingattr = NULL;
                rel->rd_pubactions = NULL;
                rel->rd_statvalid = false;
                rel->rd_statlist = NIL;
index d357ebb559804b174bfb769041ec3630c4382f68..a0ab70df8938bd022bce9927ab72f3c9c5eff0eb 100644 (file)
@@ -244,6 +244,8 @@ typedef struct IndexAmRoutine
        bool            amcaninclude;
        /* does AM use maintenance_work_mem? */
        bool            amusemaintenanceworkmem;
+       /* does AM block HOT update? */
+       bool        amhotblocking;
        /* OR of parallel vacuum flags.  See vacuum.h for flags. */
        uint8           amparallelvacuumoptions;
        /* type of data stored in index, or InvalidOid if variable */
index b4faa1c12381e9fdb0ec6e60c2a3b4b09bc12fc0..31281279cf90dc5a32371f7560387db2625f8ae2 100644 (file)
@@ -155,10 +155,11 @@ typedef struct RelationData
        List       *rd_statlist;        /* list of OIDs of extended stats */
 
        /* data managed by RelationGetIndexAttrBitmap: */
-       Bitmapset  *rd_indexattr;       /* identifies columns used in indexes */
+       bool            rd_attrsvalid;  /* are bitmaps of attrs valid? */
        Bitmapset  *rd_keyattr;         /* cols that can be ref'd by foreign keys */
        Bitmapset  *rd_pkattr;          /* cols included in primary key */
        Bitmapset  *rd_idattr;          /* included in replica identity index */
+       Bitmapset  *rd_hotblockingattr; /* cols blocking HOT update */
 
        PublicationActions *rd_pubactions;      /* publication actions */
 
index aa060ef115b26af8befe46eb9e848924e83ef257..82316bba543c8d2285b5a2c04c58677a14548746 100644 (file)
@@ -55,10 +55,10 @@ extern bytea **RelationGetIndexAttOptions(Relation relation, bool copy);
 
 typedef enum IndexAttrBitmapKind
 {
-       INDEX_ATTR_BITMAP_ALL,
        INDEX_ATTR_BITMAP_KEY,
        INDEX_ATTR_BITMAP_PRIMARY_KEY,
-       INDEX_ATTR_BITMAP_IDENTITY_KEY
+       INDEX_ATTR_BITMAP_IDENTITY_KEY,
+       INDEX_ATTR_BITMAP_HOT_BLOCKING
 } IndexAttrBitmapKind;
 
 extern Bitmapset *RelationGetIndexAttrBitmap(Relation relation,
index 5365b0639ec3369a286d7a447050fe855039fbd8..9d409faff540bbfb230a2d3303d28938c66c58ed 100644 (file)
@@ -298,6 +298,7 @@ dihandler(PG_FUNCTION_ARGS)
        amroutine->amcanparallel = false;
        amroutine->amcaninclude = false;
        amroutine->amusemaintenanceworkmem = false;
+       amroutine->amhotblocking = true;
        amroutine->amparallelvacuumoptions = VACUUM_OPTION_NO_PARALLEL;
        amroutine->amkeytype = InvalidOid;
 
index e53d6e488567cc09a7fa416ce48e28b94c5fc267..d4c03788a35b23e74616ba63975754fadb32516d 100644 (file)
@@ -567,3 +567,88 @@ SELECT * FROM brintest_3 WHERE b < '0';
 
 DROP TABLE brintest_3;
 RESET enable_seqscan;
+-- test BRIN index doesn't block HOT update
+CREATE TABLE brin_hot (
+        id  integer PRIMARY KEY,
+        val integer NOT NULL
+) WITH (autovacuum_enabled = off, fillfactor = 70);
+INSERT INTO brin_hot SELECT *, 0 FROM generate_series(1, 235);
+CREATE INDEX val_brin ON brin_hot using brin(val);
+CREATE FUNCTION wait_for_hot_stats() RETURNS void AS $$
+DECLARE
+        start_time timestamptz := clock_timestamp();
+        updated bool;
+BEGIN
+        -- we don't want to wait forever; loop will exit after 30 seconds
+        FOR i IN 1 .. 300 LOOP
+                SELECT (pg_stat_get_tuples_hot_updated('brin_hot'::regclass::oid) > 0) INTO updated;
+                EXIT WHEN updated;
+
+                -- wait a little
+                PERFORM pg_sleep_for('100 milliseconds');
+                -- reset stats snapshot so we can test again
+                PERFORM pg_stat_clear_snapshot();
+        END LOOP;
+        -- report time waited in postmaster log (where it won't change test output)
+        RAISE log 'wait_for_hot_stats delayed % seconds',
+          EXTRACT(epoch FROM clock_timestamp() - start_time);
+END
+$$ LANGUAGE plpgsql;
+UPDATE brin_hot SET val = -3 WHERE id = 42;
+-- We can't just call wait_for_hot_stats() at this point, because we only
+-- transmit stats when the session goes idle, and we probably didn't
+-- transmit the last couple of counts yet thanks to the rate-limiting logic
+-- in pgstat_report_stat().  But instead of waiting for the rate limiter's
+-- timeout to elapse, let's just start a new session.  The old one will
+-- then send its stats before dying.
+\c -
+SELECT wait_for_hot_stats();
+ wait_for_hot_stats 
+--------------------
+(1 row)
+
+SELECT pg_stat_get_tuples_hot_updated('brin_hot'::regclass::oid);
+ pg_stat_get_tuples_hot_updated 
+--------------------------------
+                              1
+(1 row)
+
+DROP TABLE brin_hot;
+DROP FUNCTION wait_for_hot_stats();
+-- Test handling of index predicates - updating attributes in precicates
+-- should block HOT even for BRIN. We update a row that was not indexed
+-- due to the index predicate, and becomes indexable.
+CREATE TABLE brin_hot_2 (a int, b int);
+INSERT INTO brin_hot_2 VALUES (1, 100);
+CREATE INDEX ON brin_hot_2 USING brin (b) WHERE a = 2;
+UPDATE brin_hot_2 SET a = 2;
+EXPLAIN (COSTS OFF) SELECT * FROM brin_hot_2 WHERE a = 2 AND b = 100;
+            QUERY PLAN             
+-----------------------------------
+ Seq Scan on brin_hot_2
+   Filter: ((a = 2) AND (b = 100))
+(2 rows)
+
+SELECT COUNT(*) FROM brin_hot_2 WHERE a = 2 AND b = 100;
+ count 
+-------
+     1
+(1 row)
+
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF) SELECT * FROM brin_hot_2 WHERE a = 2 AND b = 100;
+                 QUERY PLAN                  
+---------------------------------------------
+ Bitmap Heap Scan on brin_hot_2
+   Recheck Cond: ((b = 100) AND (a = 2))
+   ->  Bitmap Index Scan on brin_hot_2_b_idx
+         Index Cond: (b = 100)
+(4 rows)
+
+SELECT COUNT(*) FROM brin_hot_2 WHERE a = 2 AND b = 100;
+ count 
+-------
+     1
+(1 row)
+
index 3bd866d947a16e581c5ce53dff4cd70d387b5449..1d9ace83a8f4ed4b5c0bf076edbc2f90c73c8733 100644 (file)
@@ -509,3 +509,66 @@ SELECT * FROM brintest_3 WHERE b < '0';
 
 DROP TABLE brintest_3;
 RESET enable_seqscan;
+
+-- test BRIN index doesn't block HOT update
+CREATE TABLE brin_hot (
+        id  integer PRIMARY KEY,
+        val integer NOT NULL
+) WITH (autovacuum_enabled = off, fillfactor = 70);
+
+INSERT INTO brin_hot SELECT *, 0 FROM generate_series(1, 235);
+CREATE INDEX val_brin ON brin_hot using brin(val);
+
+CREATE FUNCTION wait_for_hot_stats() RETURNS void AS $$
+DECLARE
+        start_time timestamptz := clock_timestamp();
+        updated bool;
+BEGIN
+        -- we don't want to wait forever; loop will exit after 30 seconds
+        FOR i IN 1 .. 300 LOOP
+                SELECT (pg_stat_get_tuples_hot_updated('brin_hot'::regclass::oid) > 0) INTO updated;
+                EXIT WHEN updated;
+
+                -- wait a little
+                PERFORM pg_sleep_for('100 milliseconds');
+                -- reset stats snapshot so we can test again
+                PERFORM pg_stat_clear_snapshot();
+        END LOOP;
+        -- report time waited in postmaster log (where it won't change test output)
+        RAISE log 'wait_for_hot_stats delayed % seconds',
+          EXTRACT(epoch FROM clock_timestamp() - start_time);
+END
+$$ LANGUAGE plpgsql;
+
+UPDATE brin_hot SET val = -3 WHERE id = 42;
+
+-- We can't just call wait_for_hot_stats() at this point, because we only
+-- transmit stats when the session goes idle, and we probably didn't
+-- transmit the last couple of counts yet thanks to the rate-limiting logic
+-- in pgstat_report_stat().  But instead of waiting for the rate limiter's
+-- timeout to elapse, let's just start a new session.  The old one will
+-- then send its stats before dying.
+\c -
+
+SELECT wait_for_hot_stats();
+SELECT pg_stat_get_tuples_hot_updated('brin_hot'::regclass::oid);
+
+DROP TABLE brin_hot;
+DROP FUNCTION wait_for_hot_stats();
+
+-- Test handling of index predicates - updating attributes in precicates
+-- should block HOT even for BRIN. We update a row that was not indexed
+-- due to the index predicate, and becomes indexable.
+CREATE TABLE brin_hot_2 (a int, b int);
+INSERT INTO brin_hot_2 VALUES (1, 100);
+CREATE INDEX ON brin_hot_2 USING brin (b) WHERE a = 2;
+
+UPDATE brin_hot_2 SET a = 2;
+
+EXPLAIN (COSTS OFF) SELECT * FROM brin_hot_2 WHERE a = 2 AND b = 100;
+SELECT COUNT(*) FROM brin_hot_2 WHERE a = 2 AND b = 100;
+
+SET enable_seqscan = off;
+
+EXPLAIN (COSTS OFF) SELECT * FROM brin_hot_2 WHERE a = 2 AND b = 100;
+SELECT COUNT(*) FROM brin_hot_2 WHERE a = 2 AND b = 100;