How to avoid code duplication on a condition that repeats in many different queries

Several (20+) tables in my database define columns current_since::timestamptz and current_until::timestamptz. Each row in each of those tables follows the same rules:

  1. If current_since is null, the row is considered “draft”.
  2. If current_sice is not null and current_until is null, the row is considered “current after current_since moment, until forever“.
  3. If current_since and current_until is set, then the row is considered to be current between the two moments.

Each table uses a btree index on (current_since, current_until)

There are many different SQL queries where I want to ensure I only load rows current at a particular time (usually an arbitrary user input). For that, a condition might look like this (NOW() would usually be a variable holding the user input):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM record WHERE
NOW() BETWEEN record.current_since AND record.current_until
OR (
record.current_since <= NOW()
AND record.current_until IS NULL
)
</code>
<code>EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM record WHERE NOW() BETWEEN record.current_since AND record.current_until OR ( record.current_since <= NOW() AND record.current_until IS NULL ) </code>
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM record WHERE
    NOW() BETWEEN record.current_since AND record.current_until
    OR (
        record.current_since <= NOW()
        AND record.current_until IS NULL
    )

The execution plan (using SET enable_seqscan = OFF;) is as follows:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>Bitmap Heap Scan on record (cost=23.97..42.32 rows=374 width=300) (actual time=0.056..0.246 rows=374 loops=1)
Recheck Cond: (((now() >= current_since) AND (now() <= current_until)) OR ((current_since <= now()) AND (current_until IS NULL)))
Filter: (((now() >= current_since) AND (now() <= current_until)) OR ((current_since <= now()) AND (current_until IS NULL)))
Heap Blocks: exact=9
Buffers: shared hit=11
-> BitmapOr (cost=23.97..23.97 rows=374 width=0) (actual time=0.040..0.041 rows=0 loops=1)
Buffers: shared hit=2
-> Bitmap Index Scan on idx_current_at (cost=0.00..11.89 rows=1 width=0) (actual time=0.005..0.005 rows=0 loops=1)
Index Cond: ((current_since <= now()) AND (current_until >= now()))
Buffers: shared hit=1
-> Bitmap Index Scan on idx_current_at (cost=0.00..11.89 rows=374 width=0) (actual time=0.034..0.034 rows=374 loops=1)
Index Cond: ((current_since <= now()) AND (current_until IS NULL))
Buffers: shared hit=1
Planning:
Buffers: shared hit=4
Planning Time: 0.194 ms
Execution Time: 0.324 ms
</code>
<code>Bitmap Heap Scan on record (cost=23.97..42.32 rows=374 width=300) (actual time=0.056..0.246 rows=374 loops=1) Recheck Cond: (((now() >= current_since) AND (now() <= current_until)) OR ((current_since <= now()) AND (current_until IS NULL))) Filter: (((now() >= current_since) AND (now() <= current_until)) OR ((current_since <= now()) AND (current_until IS NULL))) Heap Blocks: exact=9 Buffers: shared hit=11 -> BitmapOr (cost=23.97..23.97 rows=374 width=0) (actual time=0.040..0.041 rows=0 loops=1) Buffers: shared hit=2 -> Bitmap Index Scan on idx_current_at (cost=0.00..11.89 rows=1 width=0) (actual time=0.005..0.005 rows=0 loops=1) Index Cond: ((current_since <= now()) AND (current_until >= now())) Buffers: shared hit=1 -> Bitmap Index Scan on idx_current_at (cost=0.00..11.89 rows=374 width=0) (actual time=0.034..0.034 rows=374 loops=1) Index Cond: ((current_since <= now()) AND (current_until IS NULL)) Buffers: shared hit=1 Planning: Buffers: shared hit=4 Planning Time: 0.194 ms Execution Time: 0.324 ms </code>
Bitmap Heap Scan on record  (cost=23.97..42.32 rows=374 width=300) (actual time=0.056..0.246 rows=374 loops=1)
  Recheck Cond: (((now() >= current_since) AND (now() <= current_until)) OR ((current_since <= now()) AND (current_until IS NULL)))
  Filter: (((now() >= current_since) AND (now() <= current_until)) OR ((current_since <= now()) AND (current_until IS NULL)))
  Heap Blocks: exact=9
  Buffers: shared hit=11
  ->  BitmapOr  (cost=23.97..23.97 rows=374 width=0) (actual time=0.040..0.041 rows=0 loops=1)
        Buffers: shared hit=2
        ->  Bitmap Index Scan on idx_current_at  (cost=0.00..11.89 rows=1 width=0) (actual time=0.005..0.005 rows=0 loops=1)
              Index Cond: ((current_since <= now()) AND (current_until >= now()))
              Buffers: shared hit=1
        ->  Bitmap Index Scan on idx_current_at  (cost=0.00..11.89 rows=374 width=0) (actual time=0.034..0.034 rows=374 loops=1)
              Index Cond: ((current_since <= now()) AND (current_until IS NULL))
              Buffers: shared hit=1
Planning:
  Buffers: shared hit=4
Planning Time: 0.194 ms
Execution Time: 0.324 ms

It gets bad quickly when multiple of said tables are part of a larger join query:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>SELECT * FROM record
LEFT JOIN comment
ON comment.record_id = record.id
AND (
NOW() BETWEEN comment.current_since AND comment.current_until
OR (
comment.current_since <= NOW()
AND comment.current_until IS NULL
)
)
LEFT JOIN attachment
ON attachment.comment_id = comment.id
AND (
NOW() BETWEEN attachment.current_since AND attachment.current_until
OR (
attachment.current_since <= NOW()
AND attachment.current_until IS NULL
)
)
WHERE
NOW() BETWEEN record.current_since AND record.current_until
OR (
record.current_since <= NOW()
AND record.current_until IS NULL
)
</code>
<code>SELECT * FROM record LEFT JOIN comment ON comment.record_id = record.id AND ( NOW() BETWEEN comment.current_since AND comment.current_until OR ( comment.current_since <= NOW() AND comment.current_until IS NULL ) ) LEFT JOIN attachment ON attachment.comment_id = comment.id AND ( NOW() BETWEEN attachment.current_since AND attachment.current_until OR ( attachment.current_since <= NOW() AND attachment.current_until IS NULL ) ) WHERE NOW() BETWEEN record.current_since AND record.current_until OR ( record.current_since <= NOW() AND record.current_until IS NULL ) </code>
SELECT * FROM record
    LEFT JOIN comment
        ON comment.record_id = record.id
        AND (
            NOW() BETWEEN comment.current_since AND comment.current_until
            OR (
                comment.current_since <= NOW()
                AND comment.current_until IS NULL
            )    
        )
    LEFT JOIN attachment
        ON attachment.comment_id = comment.id
        AND (
            NOW() BETWEEN attachment.current_since AND attachment.current_until
            OR (
                attachment.current_since <= NOW()
                AND attachment.current_until IS NULL
            )    
        )
WHERE
    NOW() BETWEEN record.current_since AND record.current_until
    OR (
        record.current_since <= NOW()
        AND record.current_until IS NULL
    )

^ that’s a lot of “clutter” IMO.


My question is: How can I avoid writing fairly complex conditions all the time manually? If I ever decide to change the logic, I will have to update many queries manually. Yet, I would rather do that than sacrifice performance.


I have tried to write a stored function returning a boolean:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>CREATE FUNCTION is_current(
at TIMESTAMP WITH TIME ZONE,
current_since TIMESTAMP WITH TIME ZONE,
current_until TIMESTAMP WITH TIME ZONE
)
RETURNS BOOLEAN
LANGUAGE plpgsql
AS
$$
BEGIN
IF current_since IS NULL THEN
RETURN FALSE;
END IF;
IF current_since <= at AND current_until IS NULL THEN
RETURN TRUE;
END IF;
RETURN at BETWEEN current_since AND current_until;
END
$$;
</code>
<code>CREATE FUNCTION is_current( at TIMESTAMP WITH TIME ZONE, current_since TIMESTAMP WITH TIME ZONE, current_until TIMESTAMP WITH TIME ZONE ) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ BEGIN IF current_since IS NULL THEN RETURN FALSE; END IF; IF current_since <= at AND current_until IS NULL THEN RETURN TRUE; END IF; RETURN at BETWEEN current_since AND current_until; END $$; </code>
CREATE FUNCTION is_current(
    at TIMESTAMP WITH TIME ZONE,
    current_since TIMESTAMP WITH TIME ZONE,
    current_until TIMESTAMP WITH TIME ZONE
)

    RETURNS BOOLEAN

    LANGUAGE plpgsql
AS
$$
BEGIN
    IF current_since IS NULL THEN
        RETURN FALSE;
    END IF;

    IF current_since <= at AND current_until IS NULL THEN
        RETURN TRUE;
    END IF;

    RETURN at BETWEEN current_since AND current_until;
END
$$;

Using it instead of the condition

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM record WHERE
is_current(NOW(), record.current_since, record.current_until)
</code>
<code>EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM record WHERE is_current(NOW(), record.current_since, record.current_until) </code>
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM record WHERE
    is_current(NOW(), record.current_since, record.current_until)

But it was significantly slower, as I expected:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>Seq Scan on record (cost=10000000000.00..10000000107.17 rows=125 width=300) (actual time=27.852..28.052 rows=374 loops=1)
Filter: is_current(now(), current_since, current_until)
Buffers: shared hit=9
Planning Time: 0.117 ms
JIT:
Functions: 2
Options: Inlining true, Optimization true, Expressions true, Deforming true
Timing: Generation 0.400 ms, Inlining 3.039 ms, Optimization 16.067 ms, Emission 8.698 ms, Total 28.204 ms
Execution Time: 28.509 ms
</code>
<code>Seq Scan on record (cost=10000000000.00..10000000107.17 rows=125 width=300) (actual time=27.852..28.052 rows=374 loops=1) Filter: is_current(now(), current_since, current_until) Buffers: shared hit=9 Planning Time: 0.117 ms JIT: Functions: 2 Options: Inlining true, Optimization true, Expressions true, Deforming true Timing: Generation 0.400 ms, Inlining 3.039 ms, Optimization 16.067 ms, Emission 8.698 ms, Total 28.204 ms Execution Time: 28.509 ms </code>
Seq Scan on record  (cost=10000000000.00..10000000107.17 rows=125 width=300) (actual time=27.852..28.052 rows=374 loops=1)
  Filter: is_current(now(), current_since, current_until)
  Buffers: shared hit=9
Planning Time: 0.117 ms
JIT:
  Functions: 2
  Options: Inlining true, Optimization true, Expressions true, Deforming true
  Timing: Generation 0.400 ms, Inlining 3.039 ms, Optimization 16.067 ms, Emission 8.698 ms, Total 28.204 ms
Execution Time: 28.509 ms

Bonus question: is it so much slower because using a function like this prevents the query executor from accessing the columns directly, forcing it constantly to call the function and then somehow work with the boolean result?

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật