Cross row constraints are tricky and the implementation depends on the API you present. You may be able to adapt this technique https://asktom.oracle.com/pls/apex/f?p=100:11:0::::p11_question_id:4233459000346171405 from AskTOM.
As recommended in the comments, here is a solution that works but has its limitations. When dealing with cross row constraints, my recommendation is to carefully evaluate any solution with a complete set of requirements. The question, as posted, only mentioned the INSERT limitation; however, not acting on UPDATEs and DELETEs would cause problems.
I opted for an AFTER INSERT/UPDATE/DELETE because I generally prefer to modify related tables in an AFTER trigger. That may not be the right solution for your specific use case, so caveat emptor.
The lack of deferrable constraint checking in MySQL makes this type of cross row constraint difficult to make robust. Assuming MySQL is a hard requirement, the assignment of users to teams should probably be managed by stored procedures.
CREATE TABLE team (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`member_count` INT,
`member_limit` INT NOT NULL
);
CREATE TABLE user (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT
);
CREATE TABLE team_member (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`team_id` BIGINT NOT NULL,
`member_id` BIGINT NOT NULL,
CONSTRAINT `fk_team_member_team_id` FOREIGN KEY (`team_id`)
REFERENCES `team` (`id`),
CONSTRAINT `fk_team_member_member_id` FOREIGN KEY (`member_id`)
REFERENCES `user` (`id`)
);
ALTER TABLE team
ADD CONSTRAINT enforce_team_member_limit
CHECK ((member_count >= 0) AND (member_count <= member_limit))
;
DELIMITER //
CREATE OR REPLACE TRIGGER tr_ai_update_team_size
AFTER INSERT
ON team_member
FOR EACH ROW
BEGIN
UPDATE team SET member_count = NVL(member_count,0) + 1
WHERE id=NEW.team_id;
END;
//
CREATE OR REPLACE TRIGGER tr_au_update_time_size
AFTER UPDATE
ON team_member
FOR EACH ROW
BEGIN
UPDATE team SET member_count = NVL(member_count,0) + 1
WHERE id=NEW.team_id;
UPDATE team SET member_count = NVL(member_count,0) - 1
WHERE id=OLD.team_id;
END;
//
CREATE OR REPLACE TRIGGER tr_ad_update_team_size
AFTER DELETE
ON team_member
FOR EACH ROW
BEGIN
UPDATE team SET member_count = NVL(member_count,0) - 1
WHERE id=OLD.team_id;
END;
//
DELIMITER ;
Some test statements
INSERT INTO team VALUES (1, 'Team 1', null, 3);
INSERT INTO team VALUES (2, 'Team 2', null, 2);
INSERT INTO user VALUES (1);
INSERT INTO user VALUES (2);
INSERT INTO user VALUES (3);
INSERT INTO user VALUES (4);
COMMIT;
INSERT INTO team_member VALUES (1, 1, 1);
INSERT INTO team_member VALUES (2, 1, 2);
INSERT INTO team_member VALUES (3, 1, 3);
INSERT INTO team_member VALUES (4, 1, 4);
DELETE FROM team_member where team_id=1 and member_id=3;
INSERT INTO team_member VALUES (4, 1, 4);
Here is a mockup on dbfiddle.
Pros:
Avoids having a SELECT clause in the trigger (avoiding a full index
scan or shudder full table scan plus the attendant concerns about
transaction isolation)
The check constraint enforces the limitation in the team table, thus
preventing buffoonery
The member_count column could make some routine queries more efficient
(e.g. how many people are on a team).
Cons:
I am not a big fan of having columns that are essentially derived from something else stored in the database, but I will make the trade-off for performance or constraint checking reasons. I tend to advocate for constraints because, in my experience, making the database enforce data quality prevents buffoonery by applications or users.