0

I had the following table:

CREATE TABLE transactions
(
    id NUMERIC(20, 0) NOT NULL DEFAULT NEXTVAL('transactions_sequence') PARIMARY KEY,
    transaction_id VARCHAR(255) DEFAULT NULL NULL,
    transaction_amount NUMERIC(10,0) DEFAULT NULL NULL,
    customer_name VARCHAR(256) DEFAULT NULL NULL,
    transaction_date TIMESTAMP DEFAULT NULL NULL,
    CONSTRAINT uq_transactions_transaction_id UNIQUE (transaction_id)
);

CREATE INDEX transactions_transaction_id_idx ON transactions (transaction_id, id);

and I want to partition it with transaction_date as follows:

CREATE TABLE transactions
(
    id NUMERIC(20, 0) NOT NULL DEFAULT NEXTVAL('transactions_sequence'),
    transaction_id VARCHAR(255) DEFAULT NULL NULL,
    transaction_amount NUMERIC(10,0) DEFAULT NULL NULL,
    customer_name VARCHAR(256) DEFAULT NULL NULL,
    transaction_date TIMESTAMP DEFAULT NULL NULL,
    CONSTRAINT transactions_pkey PRIMARY KEY (id, transaction_date),
    CONSTRAINT uq_transactions_transaction_id UNIQUE (transaction_id, transaction_date)
) PARTITION BY RANGE (transaction_date);

CREATE INDEX transactions_transaction_id_idx ON transactions (transaction_id, id);

with this partitioning transaction_id is not truly unique, but unique per partition, as the unique key has to have the partition key, i.e. transaction_date.

The application using this table is not aware of the partitioning, and uses 23505 SQL state to perform update instead of an insert.

I'm inserting ~300K rows per day, and the current table size is ~30 million rows.

So is the following trigger:

CREATE OR REPLACE FUNCTION transactions_validate() RETURNS TRIGGER
    LANGUAGE PLPGSQL
AS
$$
BEGIN
    IF EXISTS (SELECT id FROM transactions WHERE transaction_id = NEW.transaction_id AND id <> NEW.id) THEN
        RAISE EXCEPTION USING ERRCODE = '23505';
    END IF;
    RETURN NEW;
END;
$$;

CREATE TRIGGER transactions_validate_trigger
    BEFORE INSERT OR UPDATE
    ON transactions
    FOR EACH ROW
EXECUTE PROCEDURE transactions_validate();

a bad idea? What'd be it's performance implications?

3
  • What do you hope to achieve with partitioning?... hopefully not performance improvement of DQL statements.
    – J.D.
    Commented Oct 20, 2023 at 12:01
  • With the current rate of inserts per day, the table is expected to be 30+GB within a year. I'm not the database guy, but am more concerned with the implications on the application performance. With that said, we've experienced a huge DQL performance gain with partitioning in another case (where uniqueness was not a requirement) with similar numbers. Commented Oct 20, 2023 at 12:11
  • 1
    It sounds like in your other case you had improper indexing then, which is the correct tool for improving DQL performance. Indexing partitions the data too, but in an exponentially more efficient way. Partitioning is not meant for improving DQL performance. And it shouldn't be your first go to for trying to improve that performance, especially if it causes you to do extra inordinary things like creating your own trigger for managing uniqueness. FWIW, "30+GB within a year" is not a lot of data. I've worked with tables in the 10s of billions of rows that were 1 TB+ big, without Partitioning.
    – J.D.
    Commented Oct 20, 2023 at 13:30

1 Answer 1

1

Try to run the following three statements in two concurrent database sessions:

Session 1:

BEGIN;

INSERT INTO transactions (id, transaction_id, transaction_date)
   VALUES (1, 42, '2020-01-01 00:00:00');

Session 2:

BEGIN;

INSERT INTO transactions (id, transaction_id, transaction_date)
   VALUES (1, 42, '2023-01-01 00:00:00');

COMMIT;

Session 1:

COMMIT;

As long as the two timestamps belong in different partitions, both transactions will succeed and will violate your desired uniqueness condition.

So no, your solution is not good, even if you use deferred constraint triggers, which narrow the window for the race condition. This will only work with the SERIALIZABLE isolation level (which will cause serialization errors that require you to retry transactions) or heavy locking, which is bad for concurrency.

Usually, the way forward is to relax your uniqueness constraints. Otherwise, you will have to pay a noticeable price, as far as performance is concerned.

9
  • 1. What'd be wrong with row level pessimistic locking, if I know that I don't need concurrency per row? 2. What if I keep the UNIQUE constraint, and populate the transaction_date from the table per row before insert/update and let the constraint do the work? Commented Oct 20, 2023 at 13:34
  • Nothing is wrong with 1., if you don't mind locking all potentially conflicting rows. That will cost performance, and locking a row leads to a write. I don't understand 2. If you mean that you want to first insert the row and then update it, I think that would be a bad idea. Commented Oct 20, 2023 at 13:38
  • I see, and I don't need concurrency or performance for insert/update, as I expect little to no multiple clients requesting to update the same row. As for 2., I can have CONSTRAINT uq_transactions_transaction_id UNIQUE (transaction_id, transaction_date) and populate the NEW.transaction_date from the table before every insert/update in a trigger, and let the CONSTRAINT handle rest? Commented Oct 20, 2023 at 13:46
  • 1
    You are right, I was sloppy. Read my linked article for an example. In the case of INSERT you cannot lock the affected rows, because the rows created by concurrent inserts don't yet exists. You'd have to lock a row in a table referenced by this table, but that does not exist. So in your particular case, you'd have to lock the whole table, which is of course horrible. It would be much better to use the SERIALIZABLE isolation level. Commented Oct 21, 2023 at 5:50
  • 1
    Yes, with SERIALIZABLE your trigger is safe. Commented Oct 24, 2023 at 8:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.