The index on a date column makes sense when you are going to search the table for a given date(s), e.g.:

select * from test
where the_date = '2016-01-01';
-- or

select * from test
where the_date between '2016-01-01' and '2016-01-31';
-- etc

In these queries there is no matter whether the sort order of primary key and the date column are the same or not. Hence rewriting the data to the new table will be useless. Just create an index.

However, if you are going to use the index only in ORDER BY:

select * from test
order by the_date;

then a primary key integer index may be significantly (2-4 times) faster then an index on a date column.

Answer from klin on Stack Overflow
🌐
Reddit
reddit.com › r/postgresql › timestamp indexing yay or nay
r/PostgreSQL on Reddit: Timestamp Indexing Yay or Nay
March 20, 2024 -

Hi there,

we are setting up a Database in PostgreSQL. And in this DB we have to save a Value for every 15 min of every customer of ours. This value has to do with electic grid management, meaning it is exactly 1 value for every customer (well metering instance) for every 15min intervall (-96 per day - 35040 for every non leap Year).

As of right now we are getting these Values from a CSV Dokument per Customer that is updated every 15min. In the Future we will get this data as an XML for every 15min.

We are looking at partioning and indexing the data we get, to make the query faster.

So there will be a Table with the metadata that refers to the Value table. Now my colleague thinks we should just index the "value"-table by the timestamp, but i think this is not the optimal way. As far as i understand indexing over the timestamp would lead Postgres to rebalance the b-tree for every insert, since every insert call gives us a bunch of new values (1 value per metering instance...)

I would rather configure a custom b+-tree that suits our current and near future sitiuation. this way we do not have a lot of rebalancing.

Also it does not sit right with me that the "resolution" of a timestamp(0) is 3600 per hour instead of the necessary resolution of 4 per hour.

Another thing that bothers me, is values might be misshandeld somewhere in the APIs and the timestamp will change ever so slightly away from the exact time (e.g. 12:15:00 to 12:15:01) due to JS problems with 1/10 values or just plain human errors and this triggers a unexpected rebalancing.

As of now we have around 2000 customers for that table but are expecting to grow to around 50 000 within the next 3 years.

We are both fairly new to Postgres as you can properly tell and would really appreciate some pointers in this disscusion of ours.

🌐
GitHub
gist.github.com › cobusc › 5875282
Short explanation of the issues faced when trying to create a PostgreSQL index using the date() function and how to resolve it. · GitHub
Although this example uses UTC as the timezone value, if you have something specified differently as timezone in postgresql.conf, then you must use that value instead of UTC when creating and using the index in a query. Assuming timezone = 'localtime', then this needs to happen: CREATE INDEX ON foo (DATE(created_at AT TIME ZONE 'localtime')); EXPLAIN SELECT * FROM foo WHERE DATE(TIMEZONE('localtime'::text, created_at)) = DATE('2013-01-01'); If you don't do that, you may be deceived by erroneous query results, even though the explain would still show that it's using UTC.
🌐
Whitemiceconsulting
whitemiceconsulting.com › PosgreSQLCastedIndexes
PostgreSQL: Casted Indexes | whitemiceconsulting.com
To this end PostgreSQL supports ... INTO TABLE tstest (1,'2019-09-02 10:30:17'); INSERT 0 1 · Now we can use the "::" operator to create an index on the ts field, but as a date rather than a timestamp....
🌐
Medium
codingstory.medium.com › postgresql-index-date-of-a-timestamptz-column-cca4f313d768
PostgreSQL — Index date of a timestamptz column | by Tung Nguyen | Medium
January 5, 2021 - CREATE INDEX index_table_a_column_tz_by_date ON table_a ((timezone('UTC', column_tz)::DATE)); Then don’t forget to lock time zone for column_tz in your query too, otherwise the index won’t be used. It is reasonable because PostgreSQL doesn’t know what time zone you want to filter.
🌐
Reddit
reddit.com › r/postgresql › how to use an index only scan on a date range?
r/PostgreSQL on Reddit: How to use an index only scan on a date range?
June 18, 2021 -

I have the following query:

SELECT
  COUNT(*)
FROM
  users
INNER JOIN
  roundtables
  ON roundtables.author_id = users.id
  AND roundtables.created_at
    BETWEEN '2021-06-01' AND '2021-06-18'
  AND roundtables.deleted_at IS null
WHERE
  users.role = 'teacher'
  AND users.deleted_at IS null
;

For the date on the example, Postgres generates the following plan:

Aggregate  (cost=13120.87..13120.88 rows=1 width=8) (actual time=46.365..46.365 rows=1 loops=1)
  Buffers: shared hit=5239 read=869
  ->  Nested Loop  (cost=5048.79..13120.57 rows=120 width=0) (actual time=41.081..46.265 rows=1182 loops=1)
        Buffers: shared hit=5239 read=869
        ->  Bitmap Heap Scan on roundtables  (cost=5048.37..8750.23 rows=1098 width=16) (actual time=41.066..42.303 rows=1182 loops=1)
              Recheck Cond: ((created_at >= '2021-06-01 00:00:00+00'::timestamp with time zone) AND (created_at <= '2021-06-18 00:00:00+00'::timestamp with time zone) AND (deleted_at IS NULL))
              Heap Blocks: exact=1130
              Buffers: shared hit=1134 read=869
              ->  Bitmap Index Scan on roundtables_search_idx  (cost=0.00..5048.09 rows=1098 width=0) (actual time=40.948..40.948 rows=1182 loops=1)
                    Index Cond: ((created_at >= '2021-06-01 00:00:00+00'::timestamp with time zone) AND (created_at <= '2021-06-18 00:00:00+00'::timestamp with time zone) AND (deleted_at IS NULL))
                    Buffers: shared hit=4 read=869
        ->  Index Only Scan using users_search_idx on users  (cost=0.42..3.97 rows=1 width=16) (actual time=0.003..0.003 rows=1 loops=1182)
              Index Cond: ((id = roundtables.author_id) AND (role = 'teacher'::enum_users_role) AND (deleted_at IS NULL))
              Heap Fetches: 0
              Buffers: shared hit=4105
Planning time: 0.270 ms
Execution time: 46.401 ms

If I change the begin date to '2019-06-01', for example, it changes the plan to this:

Aggregate  (cost=56965.56..56965.57 rows=1 width=8) (actual time=259.748..259.748 rows=1 loops=1)
  Buffers: shared hit=17612 read=22067, temp read=360 written=358
  ->  Hash Join  (cost=24578.81..56938.26 rows=10921 width=0) (actual time=58.842..250.845 rows=100698 loops=1)
        Hash Cond: (roundtables.author_id = users.id)
        Buffers: shared hit=17612 read=22067, temp read=360 written=358
        ->  Seq Scan on roundtables  (cost=0.00..30543.41 rows=100222 width=16) (actual time=0.070..133.741 rows=100711 loops=1)
              Filter: ((deleted_at IS NULL) AND (created_at >= '2019-06-01 00:00:00+00'::timestamp with time zone) AND (created_at <= '2021-06-18 00:00:00+00'::timestamp with time zone))
              Rows Removed by Filter: 21183
              Buffers: shared hit=6648 read=22067
        ->  Hash  (cost=23330.01..23330.01 rows=71824 width=16) (actual time=58.262..58.262 rows=71669 loops=1)
              Buckets: 131072  Batches: 2  Memory Usage: 2718kB
              Buffers: shared hit=10964, temp written=156
              ->  Index Only Scan using users_search_idx on users  (cost=0.42..23330.01 rows=71824 width=16) (actual time=0.008..37.979 rows=71669 loops=1)
                    Index Cond: ((role = 'teacher'::enum_users_role) AND (deleted_at IS NULL))
                    Heap Fetches: 0
                    Buffers: shared hit=10964
Planning time: 0.306 ms
Execution time: 259.782 ms

So it changes what used to be a bitmap index scan + bitmap heap scan to a sequential scan on the roundtables table. I'm assuming that this is happening because the planner assumes its quicker than doing the bitmap scans.

Ideally, I would like to convert the roundtables scan to an index only scan. Is this possible, and if so, how can I achieve this?

The indexes that I am currently using are the following:

CREATE INDEX users_search_idx ON users(id, role, deleted_at);
CREATE INDEX roundtables_search_idx ON roundtables(author_id, created_at, deleted_at);
🌐
PostgreSQL
postgresql.org › message-id › D05EF808F2DFD211AE4A00105AA1B5D20378CC@cpsmail
PostgreSQL: RE: [SQL] indexing a datetime by date
March 30, 1999 - Part of the problem is that PostgreSQL Assumes that a functions value will change each time it is required, therefore automatic table scan and the function is called for each row. Try using 'now'::date instead of now()::date You index creation syntax is good but there's a bug in function indexes which require you to specify the ops. Try: create index when_ndx3 on notes (date(when) date_ops);
Find elsewhere
🌐
Medium
medium.com › nerd-for-tech › postgres-performance-tuning-with-partial-index-cffc411f719a
Postgres — Partial Index usage with Dynamic date filter | by Virender Singla | Nerd For Tech | Medium
November 23, 2022 - As we can see data is needed for last one year on the predicate column last_updated then why to create Index on complete table data (we have 20 years of data in the table). Hence we thought of creating a partial index with hard coded date (to have last 1 year data) in the WHERE clause.
🌐
Postgres Professional
postgrespro.com › list › thread-id › 1380102
Thread: Create index on the year of a date column : Postgres Professional
To summarise: 1. Use an index on the entire date column and use ranges to make use of the index. 2. Create a dummy function that wraps the extract function call, and use this as the index.
Top answer
1 of 1
35

First off, can it be? You write:

I want to fetch all data with the updated_at field with a date of a few days ago.

But your WHERE condition is:

(date(updated_at)) < (date(now())-7)

Should be >?


Indexes

For optimal performance, you could ...

  • partition your indexes
  • exclude irrelevant rows from the indexes
  • automatically recreate indexes at off-hours with updated predicate.

Your indexes could look like:

CREATE INDEX objects_id_updated_at_idx ON objects ((updated_at::date) DESC NULLS LAST)
WHERE  id BETWEEN 0 AND 999999
AND    updated_at > '2012-10-01 0:0'::timestamp;  -- some minimum date

CREATE INDEX objects_id_updated_at_idx ON objects ((updated_at::date) DESC NULLS LAST)
WHERE  id BETWEEN 1000000 AND 1999999
AND    updated_at > '2012-10-01 0:0'::timestamp;  -- some minimum date

-- etc.

(Assuming updated_at is type timestamp. With timestamptz, the cast to date is not IMMUTABLE and you need to define the time zone first ...)

The second condition excludes irrelevant rows from the index right away, which should make it smaller and faster - depending on your actual data distribution. In accordance with my preliminary comment, I am assuming you want newer rows.

The condition also automatically excludes NULL values in updated_at - which you seem to allow in the table and obviously want to exclude in the query. The usefulness of the index deteriorates over time. The query always retrieves the latest entries. Recreate the index with an updated WHERE clause periodically. This requires an exclusive lock on the table, so do it at off hours. There is also CREATE INDEX CONCURRENTLY to minimize the duration of locks:

CREATE INDEX CONCURRENTLY objects_id_up_201211_idx ...; -- create new idx
DROP INDEX CONCURRENTLY objects_id_up_201210_idx;       -- then drop old

DROP INDEX allows CONCURRENTLY since Postgres 9.2.

Related answer on SO:

  • Postgres returns records in wrong order

To further optimize, you could use CLUSTER like we mentioned in the comments. But you need a full index for that. Doesn't work with a partial index. You would create temporarily:

CREATE INDEX objects_full_idx ON objects (id/1000000, (updated_at::date) DESC NULLS LAST);

This form of the full index matches the sort order of above partial indexes.

CLUSTER objects USING objects_full_idx;
ANALYZE objects;

This will take a while, since the table is rewritten physically. It is also effectively a VACUUM FULL. It needs an exclusive write lock on the table, so do it at off-hours - provided you can afford that at all. Again, there are less invasive alternatives: pg_repack or pg_squeeze.

You can then drop the index again (if it's unused). It's a one-time effect. I would at least try this once to see how much your queries benefit from it. The effect deteriorates with subsequent write operations. You could repeat this procedure at off hours if you see a substantial effect.

If your table receives a lot of write operations, you have to weigh cost and benefit for this step. For many UPDATEs consider setting a FILLFACTOR lower than 100. Do that before you CLUSTER.

Query

SELECT count(*)
FROM   objects
WHERE  id BETWEEN 0 AND 999999  -- match conditions of partial index!
AND    updated_at > '2012-10-01 0:0'::timestamp
AND    updated_at::date > (now()::date - 7);

More

A more advanced technique for index partitioning:

  • Can spatial index help a "range - order by - limit" query

Among other things it provides example code for automatic index (re-)creation.

Make sure that autovacuum is running properly. The huge gain by CLUSTER you have reported may be due in part to the implicit vacuuming that you get from CLUSTER. Maybe this is set up by Heroku automatically, not sure.
The settings in your question look good. So that's probably not an issue here and CLUSTER really was that effective.

Declarative partitioning

has finally matured in Postgres 12. I would consider using that now instead of manual index partitioning (or at least additionally). Range partitioning with updated_at as the partition key. There are also multiple improvements to general performance, big data and B-tree index performance in particular.

Top answer
1 of 3
87

Don’t worry, be happy

how does postgres perform the index on the timestamp - in other words will it index the timestamp based on the date/ time, or will it go into the seconds/ milliseconds, etc?

The internals of the indexing scheme used by Postgres should generally be transparent to you, of no regard. And keep in mind that the implementation you study today may change in a future version of Postgres.

You are likely falling into the trap of premature optimization. Trust Postgres and its default behaviors until you know you have a demonstrable performance problem.

Moments

Date-time handling is more complicated than you might understand.

Firstly, you are using TIMESTAMP which is actually the abbreviated name for TIMESTAMP WITHOUT TIME ZONE. This type cannot represent a moment. This type stores only a date and a time-of-day. For example, January 23 2020 at 12:00 noon. But does that mean noon in Tokyo Japan? Or noon in Paris France, several hours later? Or noon in Toledo Ohio US, several more hours later?

I suggest always expanding the type name out fully to be very clear in your SQL. Use TIMESTAMP WITHOUT TIME ZONE rather than TIMESTAMP.

But if you are actually trying to represent moments, a specific point on the timeline, you must use the TIMESTAMP WITH TIME ZONE. This name comes from the SQL Standard. But in Postgres, and some other databases, it is a bit of a misnomer. Postgres does not actually store the time zone. Instead, Postgres uses any time zone or offset-from-UTC information submitted along with an input to adjust into UTC. The value written to storage is always in UTC. If you care about the original zone name or offset numbers (hours-minutes-seconds), then you need to store that in a second column.

When retrieved from the database, the value comes out in UTC as well. But be aware that some middleware tools insist on applying a default time zone the value after retrieval. While well-intentioned, this anti-feature can cause much confusion. You will have no such confusion when using java.time objects as shown below.

Span-of-time queries

Postgres is storing a moment in UTC, likely as a count from an epoch-reference date-time given that the data type is documented as being an integer of 64 bits (8 octets). According to Wikipedia, Postgres uses an epoch reference of 2000-01-01, presumably the first moment of that date in UTC, 2000-01-01T00:00:00.0Z. We do not have any reason to care about what epoch references is used, but there you go.

The real point is that a date-time value in Postgres is stored simply as a number, a count of microseconds. The timestamp types are not a specific date and a time-of-day as you may be thinking. Your queries certainly may benefit from an index on the timestamp column, but date-oriented (without time-of-day) queries will not benefit specifically. The index is not date-oriented, nor can it be as I will explain next.

Determining a date from a moment requires a time zone. For any given moment, the date varies around the globe by time zone. A few minutes after midnight in Paris France is a new day while still “yesterday” in Montréal Québec.

To query moments by date, you need to determine the first moment of the day, and the first moment of the following day. Then we use the Half-Open approach to defining a span-of-time where the beginning is inclusive while the ending is exclusive. We search for moments that are equal to or later than the beginning while also being before the ending. Tip: another way of saying "equal to or later than the beginning" is "not before".

You are using Java, so you can make use of the industry-leading java.time classes there.

The java.time classes use a resolution of nanoseconds, finer than the microseconds used in Postgres. So you will have no problem loading Postgres values into Java. However, beware of data loss when going the other direction as nanoseconds will be silently truncated to store only microseconds.

When determining the first moment of the day, do not assume the day starts at 00:00:00.0. Some dates in some zones start at another time such as 01:00:00.0. Always let java.time determine the first moment of the day.

ZoneId z = ZoneId.of( "Asia/Tokyo" ) ;                          // Or `Africa/Tunis`, `America/Montreal`, etc.
LocalDate today = LocalDate.now( z ) ;
ZonedDateTime zdtStart = today.atStartOfDay( z ) ;              // First moment of the day.
ZonedDateTime zdtStop = today.plusDays( 1 ).atStartOfDay( z ) ; // First moment of the following day.

Write your Half-Open SQL statement. Do not use the SQL command BETWEEN because it is not Half-Open.

String sql = "SELECT * FROM tbl WHERE event !< ? && event < ? ;" ;  // Half-Open query in SQL.

Pass your beginning and ending values to a prepared statement.

Your JDBC driver supporting JDBC 4.2 and later can work with most java.time by using PreparedStatement::setObject & ResultSet::getObject. Oddly, the JDBC spec does not require support for the two most commonly used types: Instant (always in UTC) and ZonedDateTime. These may or may not work your specific driver. The standard does require support for OffsetDateTime, so let's convert to that.

preparedStatement.setObject( 1 , zdtStart.toOffsetDateTime() ) ;
preparedStatement.setObject( 2 , zdtStop.toOffsetDateTime() ) ;

The resulting OffsetDateTime objects passed to the PreparedStatement will carry the offset used by that time zone at that date-time. For debugging, or curiosity, you may want to see those values in UTC. So let's adjust to UTC by extracting a Instant, and then applying an offset of zero hours-minutes-seconds to get a OffsetDateTime carrying an offset of UTC itself.

OffsetDateTime start = zdtStart.toInstant().atOffset( ZoneOffset.UTC ) ;
OffsetDateTime stop = zdtStop.toInstant().atOffset( ZoneOffset.UTC ) ;

Pass to the prepared statement.

preparedStatement.setObject( 1 , start ) ;
preparedStatement.setObject( 2 , stop ) ;

Once these start and stop values arrive at the database server, they will be converted to a number representing a count-from-epoch, a simple integer. Then Postgres performs a simple number comparison. If an index exists on those integer numbers, that index may or may not be utilized as the Postgres query planner sees fit.

If you have relatively small number of rows, and much RAM to cache them, you may not need an index. Perform tests, and use EXPLAIN/ANALYZE to see the real-world performance.

Date column via Java

If you have done the work to prove a performance problem with date-oriented queries, you could add a second column of type DATE. Then index that column, and refer to it explicitly in your date-oriented queries.

When inserting your moment, also include a calculated value for the date as perceived in whatever time zone makes sense to your app. Just be sure to clearly document your intentions, and the specifics of the time zone used in determining the date. Tip: Postgres offers a feature to include a blurb of text as part of your column's definition alongside the column name and its data type.

As a second DATE column is derived from another column, it is by definition redundant and there de-normalized. As a rule, you should consider de-normalizing only as a last resort.

Java code when inserting a value.

String sql = "INSERT INTO tbl ( event , date_tokyo ) VALUES ( ? , ? ) ;" ;

Determine the current moment, and the current moment's date as perceived in time zone Asia/Tokyo.

Instant now = Instant.now() ;  // Always in UTC, no need to specify a time zone here.
OffsetDateTime odt = now.atOffset( ZoneOffset.UTC ) ;  // Convert from `Instant` to `OffsetDateTime` if your JDBC driver does not support `Instant`.
ZoneId z = ZoneId.of( "Asia/Tokyo" ) ;
ZonedDateTime zdt = now.atZone( z ) ;
LocalDate localDate = zdt.toLocalDate() ; // Extract the date as seen at this moment by people in the Tokyo time zone.

Pass to your prepared statement.

preparedStatement.setObject( 1 , odt ) ;
preparedStatement.setObject( 2 , localDate ) ;

Now you can make date-oriented queries on the date_tokyo column. Index if need be.

Date column via SQL

Alternatively, you could populate that date_tokyo column automatically within Postgres.

Trigger

You could write a trigger that uses date-time functions built into Postgres to determine the date of that moment as seen in the time zone Asia/Tokyo. The trigger could then write the resulting date value into that second column.

Generated value column

Or, with Postgres 12, you can more simply use the new generated columns feature. This new feature does the same work, but without the bother of defining and attaching a trigger. For discussion of this new feature, see:

  • New In PostgreSQL 12: Generated Columns
  • Generated columns in PostgreSQL 12 
by Kirk Roybal
  • PostgreSQL 12: generated columns 
by Daniel Westermann

In Postgres 12, a column with GENERATED ALWAYS AS (…) STORED has its value physically stored, and can be indexed.

Caveat

Crucial to such date-time work is correct information about the current definitions of time zones. Usually this information comes via the tz data maintained by ICANN/IANA.

Both Java and Postgres contain their own copy of tz data.

Politicians around the world have shown a penchant for redefining their time zones, often with little or no warning. So be sure to keep track of changes to time zones you care about. When you update Java or Postgres, you will likely be getting fresh copy of the tz data. But in some cases you may need to manually update either or both environments (Java & Postgres). And your host OS has a tz data copy as well, fyi.

2 of 3
22

This is a regurgitation of what Percona recommends.

They recommend the

BRIN index

.

I needed this proof for picking up sets of records ordered by timestamptz. Even though the example uses timestamp I use timestamptz.

  1. And my records are chronological and old timestamptz columns are not updated or deleted.

  2. Other columns in only recent records are updated. Older records are untouched.

My table will have a few million records.

You could test your queries. I use pgAdmin.

CREATE TABLE testtab (id int NOT NULL PRIMARY KEY,date TIMESTAMP NOT NULL, level INTEGER, msg TEXT);

create index testtab_date_idx  on testtab(date);

"Gather  (cost=1000.00..133475.57 rows=1 width=49) (actual time=848.040..862.638 rows=0 loops=1)"
"  Workers Planned: 2"
"  Workers Launched: 2"
"  ->  Parallel Seq Scan on testtab  (cost=0.00..132475.47 rows=1 width=49) (actual time=832.108..832.109 rows=0 loops=3)"
"        Filter: ((date >= '2019-08-08 14:40:47.974791'::timestamp without time zone) AND (date <= '2019-08-08 14:50:47.974791'::timestamp without time zone))"
"        Rows Removed by Filter: 2666667"
"Planning Time: 0.238 ms"
"Execution Time: 862.662 ms"

explain analyze select * from public.testtab where date between '2019-08-08 14:40:47.974791' and '2019-08-08 14:50:47.974791';

"Gather  (cost=1000.00..133475.57 rows=1 width=49) (actual time=666.283..681.586 rows=0 loops=1)"
"  Workers Planned: 2"
"  Workers Launched: 2"
"  ->  Parallel Seq Scan on testtab  (cost=0.00..132475.47 rows=1 width=49) (actual time=650.661..650.661 rows=0 loops=3)"
"        Filter: ((date >= '2019-08-08 14:40:47.974791'::timestamp without time zone) AND (date <= '2019-08-08 14:50:47.974791'::timestamp without time zone))"
"        Rows Removed by Filter: 2666667"
"Planning Time: 0.069 ms"
"Execution Time: 681.617 ms"

create index testtab_date_brin_idx  on rm_owner.testtab using brin (date);

explain analyze select * from public.testtab where date between '2019-08-08 14:40:47.974791' and '2019-08-08 14:50:47.974791';

"Bitmap Heap Scan on testtab  (cost=20.03..33406.84 rows=1 width=49) (actual time=0.143..0.143 rows=0 loops=1)"
"  Recheck Cond: ((date >= '2019-08-08 14:40:47.974791'::timestamp without time zone) AND (date <= '2019-08-08 14:50:47.974791'::timestamp without time zone))"
"  ->  Bitmap Index Scan on "testtab_date_brin_idx "  (cost=0.00..20.03 rows=12403 width=0) (actual time=0.141..0.141 rows=0 loops=1)"
"        Index Cond: ((date >= '2019-08-08 14:40:47.974791'::timestamp without time zone) AND (date <= '2019-08-08 14:50:47.974791'::timestamp without time zone))"
"Planning Time: 0.126 ms"
"Execution Time: 0.161 ms"

Update : All the examples I see are like the one described here

🌐
Crunchy Data Blog
crunchydata.com › blog › postgresql-brin-indexes-big-data-performance-with-minimal-storage
PostgreSQL BRIN Indexes: Big Data Performance With Minimal Storage | Crunchy Data Blog
DROP INDEX scans_created_at_idx; CREATE INDEX scans_created_at_brin_idx ON scans USING brin(created_at); Time: 18396.309 ms (00:18.396) VACUUM FREEZE ANALYZE; The BRIN index appears to take a lot less time to create than the B-tree index. Given I am running PostgreSQL 11, I have the added benefit of parallelized B-tree index creation too, so you can see really how efficiently BRIN index creation can be! Now, let's see how the query performs. When I run: EXPLAIN ANALYZE SELECT date_trunc('day', created_at), avg(scan) FROM scans WHERE created_at BETWEEN '2017-02-01 0:00' AND '2017-02-28 11:59:59' GROUP BY 1 ORDER BY 1;
🌐
PostgreSQL
postgresql.org › message-id › 1012707.1621880687@sss.pgh.pa.us
PostgreSQL: Re: Creating an index on a timestamp with time zone cast to a date-- possible?
May 24, 2021 - Wells Oliver <wells(dot)oliver(at)gmail(dot)com> writes: > Seems you can't do this: > create index on someschema.sometable ((modified_at::date)); > If modified_at is a timestamp with a time zone: > ERROR: functions in index expression must be marked IMMUTABLE > But you can if it's a timestamp without a time zone.
🌐
PostgreSQL
postgresql.org › docs › current › indexes-ordering.html
PostgreSQL: Documentation: 18: 11.4. Indexes and ORDER BY
May 14, 2026 - 11.4. Indexes and ORDER BY # In addition to simply finding the rows to be returned by a query, an index …
Top answer
1 of 4
16

Usually database optimizers won't chose to use indexes for open-ended ranges, such a updated_at > somedate.

But, in many cases the datatime column won't exceed "now", so you can preserve the semantic of > somedate by converting the condition to a range by using between like this:

where updated_at between somedate and current_timestamp

A between predicate is much more likely to cause the optimizer to chose to use an index.


Please post if this approach improved your query’s performance.

2 of 4
6

For any given query, the use of an index depends on the cost of using that index compared to a sequential scan

Frequently developers think that because there is an index, a query should run faster, and if a query runs slow, an index is the solution. This is usually the case when the query will return few tuples. But as the number of tuples in the result increases, the cost of using an index might increase.

You are using postgres. Postgres does not support clustering around a given attribute. That means that postgres, when confronted with a range query (of the type att > a and att < b) needs to compute an estimation of the number of tuples in the result (make sure you vacuum your database frequently) and the cost of using an index compared to doing a sequential scan. it will then decide what method to use.

you can inspect this decision by running

EXPLAIN ANALYZE <query>; 

in psql. It will tell you if it uses an index or not.

If you really, really want to use the indexes instead of a sequential scan (sometimes it is needed) and you really really know what you are doing, you can change the cost of a sequential scan in the planner constants or disable sequential scans in favor of any other method. See this page for the details:

http://www.postgresql.org/docs/9.1/static/runtime-config-query.html

Make sure you browse the correct version of the documentation.

--dmg