Đầu tiên, lưu ý rằng giải pháp lý tưởng phụ thuộc vào mức độ RDBMS mà bạn sử dụng. Tôi sẽ đưa ra cả câu trả lời cụ thể và tiêu chuẩn cụ thể của PostgreSQL.
Chuẩn hóa, trả lời chuẩn
Câu trả lời tiêu chuẩn là có hai bảng tham gia.
Giả sử chúng ta có các bảng:
CREATE TABLE keywords (
kword text
);
CREATE TABLE reports (
id serial not null unique,
...
);
CREATE TABLE recommendations (
id serial not null unique,
...
);
CREATE TABLE report_keywords (
report_id int not null references reports(id),
keyword text not null references keyword(kword),
primary key (report_id, keyword)
);
CREATE TABLE recommendation_keywords (
recommendation_id int not null references recommendation(id),
keyword text not null references keyword(kword),
primary key (recommendation_id, keyword)
);
Cách tiếp cận này tuân theo tất cả các quy tắc chuẩn hóa tiêu chuẩn và không phá vỡ các nguyên tắc chuẩn hóa cơ sở dữ liệu truyền thống. Nó nên hoạt động trên bất kỳ RDBMS nào.
Trả lời dành riêng cho PostgreSQL, thiết kế N1NF
Đầu tiên, một từ về lý do tại sao PostgreSQL là khác nhau. PostgreSQL hỗ trợ một số cách rất hữu ích để sử dụng các chỉ mục trên các mảng, đáng chú ý nhất là sử dụng các chỉ mục GIN. Chúng có thể mang lại lợi ích hiệu quả khá nhiều nếu được sử dụng đúng cách ở đây. Bởi vì PostgreSQL có thể "tiếp cận" các loại dữ liệu theo cách này, giả định cơ bản về tính nguyên tử và chuẩn hóa có phần khó giải quyết khi áp dụng một cách cứng nhắc ở đây. Vì vậy, vì lý do này, khuyến nghị của tôi là phá vỡ quy tắc nguyên tử của mẫu thông thường đầu tiên và dựa vào các chỉ số GIN để có hiệu suất tốt hơn.
Một lưu ý thứ hai ở đây là trong khi điều này mang lại hiệu suất tốt hơn, nó gây thêm một số vấn đề đau đầu vì bạn sẽ có một số công việc thủ công phải làm để có được tính toàn vẹn tham chiếu để hoạt động tốt. Vì vậy, sự đánh đổi ở đây là hiệu suất cho công việc thủ công.
CREATE TABLE keyword (
kword text primary key
);
CREATE FUNCTION check_keywords(in_kwords text[]) RETURNS BOOL LANGUAGE SQL AS $$
WITH kwords AS ( SELECT array_agg(kword) as kwords FROM keyword),
empty AS (SELECT count(*) = 0 AS test FROM unnest($1))
SELECT bool_and(val = ANY(kwords.kwords))
FROM unnest($1) val
UNION
SELECT test FROM empty WHERE test;
$$;
CREATE TABLE reports (
id serial not null unique,
...
keywords text[]
);
CREATE TABLE recommendations (
id serial not null unique,
...
keywords text[]
);
Bây giờ chúng tôi phải thêm các kích hoạt để đảm bảo rằng các từ khóa được quản lý đúng cách.
CREATE OR REPLACE FUNCTION trigger_keyword_check() RETURNS TRIGGER
LANGUAGE PLPGSQL AS
$$
BEGIN
IF check_keywords(new.keywords) THEN RETURN NEW
ELSE RAISE EXCEPTION 'unknown keyword entered'
END IF;
END;
$$;
CREATE CONSTRAINT TRIGGER check_keywords AFTER INSERT OR UPDATE TO reports
WHEN (old.keywords <> new.keywords)
FOR EACH ROW EXECUTE PROCEDURE trigger_keyword_check();
CREATE CONSTRAINT TRIGGER check_keywords AFTER INSERT OR UPDATE
TO recommendations
WHEN (old.keywords <> new.keywords)
FOR EACH ROW EXECUTE PROCEDURE trigger_keyword_check();
Thứ hai, chúng ta phải quyết định phải làm gì khi một từ khóa bị xóa. Vì hiện tại, một từ khóa được xóa khỏi bảng từ khóa sẽ không xếp tầng cho các trường từ khóa. Có lẽ đây là mong muốn và có thể không. Điều đơn giản nhất để làm là chỉ cần hạn chế xóa luôn và mong bạn sẽ tự xử lý trường hợp này nếu nó xuất hiện (sử dụng trình kích hoạt để đảm bảo an toàn tại đây). Một tùy chọn khác có thể là viết lại mọi giá trị từ khóa trong đó từ khóa tồn tại để loại bỏ nó. Một lần nữa kích hoạt sẽ là cách để làm điều đó là tốt.
Ưu điểm lớn của giải pháp này là bạn có thể lập chỉ mục cho việc tra cứu rất nhanh theo từ khóa và bạn có thể kéo tất cả các thẻ mà không cần tham gia. Nhược điểm là loại bỏ một từ khóa là một nỗi đau, và sẽ không hoạt động tốt ngay cả vào một ngày tốt. Điều này có thể được chấp nhận vì đây là một sự kiện hiếm gặp và có thể được giao cho một quá trình nền nhưng nó là một sự đánh đổi đáng để hiểu.
Phê bình giải pháp đầu tiên của bạn
Vấn đề thực sự với giải pháp đầu tiên của bạn là bạn không có khóa nào có thể có trên ObjectKeywords. Do đó, bạn có một vấn đề trong đó bạn không thể đảm bảo rằng mỗi từ khóa được áp dụng cho mỗi đối tượng chỉ một lần.
Giải pháp thứ hai của bạn tốt hơn một chút. Nếu bạn không thích các giải pháp khác được cung cấp, tôi khuyên bạn nên đi với nó. Tuy nhiên tôi sẽ đề nghị loại bỏ keyword_id và chỉ tham gia vào văn bản từ khóa. Điều đó giúp loại bỏ một tham gia mà không chuẩn hóa.