How to get unique, non-overlapping, contiguous ranges, usable by concurrent sessions?



An example use-case might be that multiple threads need to retrieve a range of ID values that have not been used yet (are unique), are contiguous (no gaps), and without any overlaps. This example makes use of a mutex table, using the single-row constraint demonstrated in an earlier post.

DROP TABLE IF EXISTS single_row_table;

CREATE TABLE single_row_table ( id INTEGER NOT NULL );


-- This unique index enforces that only a single row can ever be in this table.
CREATE UNIQUE INDEX one_row_only_uidx ON single_row_table (( true ));

INSERT INTO single_row_table (id) VALUES (1);

-- Drop sequence if it already exists
DROP SEQUENCE IF EXISTS single_row_table_seq;

-- Create the sequence we will use to generate the ranges
CREATE SEQUENCE single_row_table_seq;


-- Let's assume we want 5 values at a time
-- Note: the "begin;commit;" is optional,
--  it is used to demonstrate multiple sessions waiting.
-- Alternatively, for testing, add "pg_sleep(10)" (no quotes)
--  to the SELECT below
begin;
SELECT string_agg(id::text,',' ORDER BY 1) as id_range
FROM (
    SELECT nextval('single_row_table_seq') AS id
    FROM generate_series(1, 5) n
    JOIN LATERAL (
        /* block access to the row in single_row_table */
        SELECT id
        FROM single_row_table
        FOR UPDATE) x ON true
    ) y;


-- session 1 immediately returns:
 id_range
-----------
 1,2,3,4,5

commit;

-- session 2 waits until session 1 commits,
-- then returns:
  id_range
------------
 6,7,8,9,10

The caveat is that concurrent callers will block until the transaction in the earlier session(s) commits, but since that should be less than a millisecond, it shouldn't be a problem unless there are many concurrent processes trying to get ranges. A possible, if unlikely, problem is that the blocked session count may exceed the max_connections setting. The original use-case was a requirement to send files to an external financial provider, where each file needed to include the same count of IDs, but they could not overlap, nor have gaps in the range.