0

I have a Payments table, with paymentDate and licenseId attributes. I want to make sure that, there is only one payment for a licenseId every month. How can I do that? I have seen an answer which says to store incomplete date (day as 00) and make it unique. But, I need exact date too.

I have tried the below things but in vain.

PRIMARY KEY (`licenseId`, YEAR(`paymentDate`), MONTH(`paymentDate`))
UNIQUE (`licenseId`, YEAR(`paymentDate`), MONTH(`paymentDate`))

I am looking for ways, which doesn't include extra year and month attributes in table.

8
  • The primary key is "too important" to use expressions probably, but an UNIQUE key might allow that (can't see it saying otherwise from a quick check of the manual - dev.mysql.com/doc/refman/8.0/en/… )
    – jkavalik
    Commented Oct 24, 2021 at 11:31
  • @jkavalik why is it "too important"? If the data fits, by all means use it as a PK - no surrogate key overhead, natural key - why not?
    – Vérace
    Commented Oct 24, 2021 at 11:42
  • @Vérace "too important" in InnoDB structure to be defined on an expression instead of an actual column - dbfiddle.uk/…
    – jkavalik
    Commented Oct 24, 2021 at 11:43
  • @Vérace, PRIMARY KEY's purpose is to make sure that rows are unique. So, It makes sense that functional key is not accepted by it. Commented Oct 24, 2021 at 11:46
  • @AppajiChintimi thats not the reason - the unique key in the answer does the same thing and is mostly equivalent to actual PK, the limitation about expressions and PK is technical in nature and afaik specific to mysql-innodb. It could be implemented most probably but they decided not to (currently at least).
    – jkavalik
    Commented Oct 24, 2021 at 11:49

2 Answers 2

3

You can use functions to obtain PRIMARY KEYs by using GENERATED (or COMPUTED or CALCULATED and sometimes VIRTUAL) fields. However, the virtual keyword also refers to the storage class (STORED or VIRTUAL) meaning that the generated field is either kept on disk or calculated on-the-fly.

See the fiddle here:

You can have your (licenceID, YEAR(paymentDate), MONTH(paymentDate)) as the PRIMARY KEY as follows:

CREATE TABLE t
(
  i INT, 
  a DATE,
  a_y SMALLINT GENERATED ALWAYS AS  (YEAR(a)) STORED,
  a_m SMALLINT GENERATED ALWAYS AS (MONTH(a)) STORED,
  
  CONSTRAINT t_pk PRIMARY KEY (i, a_y, a_m)
);

and then test:

INSERT INTO t (i, a) VALUES (1, NOW());                               ✓
INSERT INTO t (i, a) VALUES (1, DATE_ADD(NOW(),  INTERVAL 1 MONTH));  ✓
INSERT INTO t (i, a) VALUES (2, now());                               ✓

but:

INSERT INTO t (i, a) VALUES (1, '2021-10-30');

gives

Duplicate entry '1-2021-10' for key 't.PRIMARY'

Creating a table with a PRIMARY KEY using direct calls to the functions isn't possible:

CREATE TABLE s
(
  i INT, 
  a DATE,
  a_y SMALLINT GENERATED ALWAYS AS (YEAR(a)) STORED,
  a_m SMALLINT GENERATED ALWAYS AS (MONTH(a)) STORED,
  
  CONSTRAINT t_pk PRIMARY KEY (i, (YEAR(a)), (MONTH(a)))
);

gives:

The primary key cannot be a functional index

Doesn't work in PostgreSQL either. In MySQL, it won't work for the VIRTUAL storage class (which PostgreSQL doesn't support anyway).

3
  • Good idea! Still I would consider the generated columns extra attributes :)
    – jkavalik
    Commented Oct 24, 2021 at 12:48
  • With disk space being so cheap, it's hardly a big deal! :-) I did look at VIRTUAL as the storage type, but it doesn't work!
    – Vérace
    Commented Oct 24, 2021 at 12:52
  • Yeah, virtual column are more of a syntactical sugar over expressions.
    – jkavalik
    Commented Oct 24, 2021 at 12:53
1

With MySQL 8.0.13+ functional key parts should work fine:

CREATE TABLE `table` (
  `licenseId` int,
  `paymentDate` date DEFAULT NULL,
  UNIQUE KEY `c` (`licenseId`,(year(`paymentDate`)),(month(`paymentDate`)))
);

Can be tested online at https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=fef176b2ec3a68a1e45acbca122339f7

On the other hand the Primary key cannot be defined that way (tested here). So you need to add some other - autoinc or similar, or possibly even (licenseId, paymentDate) would be fine as the PK.

2
  • Thank you! Working as expected. Apparently, having parenthesis is different from not having :/ Commented Oct 24, 2021 at 11:44
  • 1
    Check out my answer - you can use generated fields in PKs - MySQL is just a bit fussy how you do this!
    – Vérace
    Commented Oct 24, 2021 at 12:44

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.