Let\'s say I have two tables with data that isn\'t supposed to change through the use of my application (i.e. domain tables) for something like the model of a car
When modeling a many to many relationship, as Kleskowy stated in the comments, you need one entry in the join table per relationship (model <-> color).
You could reproduce your example in the following way:
CREATE TABLE car_model(car_model_id serial NOT NULL PRIMARY KEY, model_name text NOT NULL);
CREATE TABLE car_color(car_color_id serial NOT NULL PRIMARY KEY, color_name text NOT NULL);
CREATE TABLE join_model_color(
car_model_id int NOT NULL REFERENCES car_model(car_model_id),
car_color_id int NOT NULL REFERENCES car_color(car_color_id),
CONSTRAINT model_color_unique_entries UNIQUE(car_model_id, car_color_id)
);
Then to insert your data:
INSERT INTO car_model(model_name) SELECT 'Volvo V70' UNION SELECT 'Volvo V60';
INSERT INTO car_color(color_name) SELECT 'Pearl White' UNION SELECT 'Emerald Green' UNION SELECT 'Night Black' UNION SELECT 'Salmon Pink' UNION SELECT 'Ocean Blue' UNION SELECT 'Raspberry Red';
INSERT INTO join_model_color SELECT car_model_id, car_color_id FROM car_model, car_color WHERE model_name = 'Volvo V70' AND color_name IN ('Pearl White', 'Emerald Green', 'Night Black', 'Salmon Pink', 'Ocean Blue', 'Raspberry Red');
INSERT INTO join_model_color SELECT car_model_id, car_color_id FROM car_model, car_color WHERE model_name = 'Volvo V60' AND color_name IN ('Pearl White', 'Emerald Green', 'Night Black', 'Salmon Pink', 'Ocean Blue', 'Raspberry Red');
Then you can query your data for example like this:
SELECT color_name FROM car_model NATURAL INNER JOIN join_model_color NATURAL INNER JOIN car_color WHERE model_name = 'Volvo V70';
SELECT model_name FROM car_model NATURAL INNER JOIN join_model_color NATURAL INNER JOIN car_color WHERE color_name = 'Emerald Green';
If models don't share colour groups then the design would be one table:
model [model] comes in color [color]
If models share colour groups then have two tables:
model [model] comes in the colors of group [group]
group [group] has color [color]
These tables join with projection to the first table:
SELECT model, color FROM model_group NATURAL JOIN group_color
If a model can have exceptional available and/or unavailable colours in addition to or instead of a group then have exception tables. A table's group is now its default colours (if any):
model [model] has default color group [group]
group [group] has color [color]
model [model] is exceptionally available in color [color]
model [model] is exceptionally unavailable in color [color]
The exception tables are then respectively UNIONed with and MINUSed/EXCEPTed from a JOIN-plus-PROJECT/SELECT to give the first table:
SELECT group, color FROM model_default NATURAL JOIN group_colour
EXCEPT SELECT * FROM model_unavailable
UNION SELECT * FROM model_available
"Redundancy" is not about values appearing in multiple places. It is about multiple rows stating the same thing about the application.
Every table (and query expression) has an associated fill-in-the-(named-)blanks statement template (aka predicate). The rows that make a true statement go in the table. If you have two independent predicates then you need two tables. The relevant values go in the rows of each one.
Re rows making statements about the application see this. (And search my other answers re a table's "statement" or "criterion".) Normalization helps because it replaces tables whose rows state things of the form "... AND ..." by other tables that state the "..." separately. See this and this.
If you share groups and only use a single two-column table for model and color then its predicate is:
FOR SOME group
model [model] comes in the colors of group [group]
AND group [group] has color [color]
So the second bullet removes a single "AND" from this predicate, ie the source of a "multivalued dependency". Otherwise if you change a model's group or a group's colours then you have to simultaneously consistently change multiple rows. (The point is to reduce errors and complexity from redundancy, not save space.)
If you don't want to repeat the strings for implementation(-dependent) reasons (space taken or speed of operations at the expense of more joins) then add a table of name ids and strings and replace your old name columns and values by id columns and values. (That's not normalization, that's complicating your schema for the sake of implementation-dependent data optimization tradeoffs. And you should demonstrate this is needed and works.)