Desain Basis Data - berbagai objek dengan penandaan bersama

8

Latar belakang saya lebih pada pemrograman web daripada administrasi basis data, jadi tolong perbaiki saya jika saya menggunakan terminologi yang salah di sini. Saya mencoba mencari cara terbaik untuk mendesain database untuk aplikasi yang akan saya koding.

Situasi: Saya punya Laporan di satu meja dan Rekomendasi di meja lain. Setiap Laporan dapat memiliki banyak Rekomendasi. Saya juga memiliki tabel terpisah untuk Kata Kunci (untuk menerapkan penandaan). Namun, saya ingin memiliki hanya satu set kata kunci yang diterapkan pada Laporan dan Rekomendasi sehingga pencarian pada kata kunci memberi Anda Laporan dan Rekomendasi sebagai hasil.

Inilah struktur yang saya mulai dengan:

Reports
----------
ReportID
ReportName


Recommendations
----------
RecommendationID
RecommendationName
ReportID (foreign key)


Keywords
----------
KeywordID
KeywordName


ObjectKeywords
----------
KeywordID (foreign key)
ReportID (foreign key)
RecommendationID (foreign key)

Secara naluriah, saya merasa seperti ini tidak optimal dan saya harus memiliki objek taggable saya mewarisi dari orang tua yang sama, dan meminta orang tua komentar itu ditandai, yang akan memberikan struktur berikut:

BaseObjects
----------
ObjectID (primary key)
ObjectType


Reports
----------
ObjectID_Report (foreign key)
ReportName


Recommendations
----------
ObjectID_Recommendation (foreign key)
RecommendationName
ObjectID_Report (foreign key)


Keywords
----------
KeywordID (primary key)
KeywordName


ObjectKeywords
----------
ObjectID (foreign key)
KeywordID (foreign key)

Haruskah saya pergi dengan struktur kedua ini? Apakah saya melewatkan masalah penting di sini? Juga, jika saya menggunakan yang kedua, apa yang harus saya gunakan sebagai nama non-generik untuk menggantikan "Objek"?

Memperbarui:

Saya menggunakan SQL Server untuk proyek ini. Ini adalah aplikasi internal dengan sejumlah kecil pengguna non-konkuren, jadi saya tidak mengantisipasi beban tinggi. Dalam hal penggunaan, kata kunci kemungkinan akan digunakan hemat. Cukup banyak hanya untuk keperluan pelaporan statistik. Dalam hal itu, solusi apa pun yang saya gunakan mungkin hanya akan memengaruhi pengembang mana pun yang perlu mempertahankan sistem ini di masa depan ... tapi saya pikir sebaiknya menerapkan praktik yang baik kapan pun saya bisa. Terima kasih atas semua wawasannya!

matikin9
sumber
Sepertinya Anda tidak memiliki pertanyaan paling penting yang dijawab - Bagaimana data diakses? - Untuk pertanyaan / pernyataan mana yang Anda ingin "sesuaikan" dengan model Anda? - Bagaimana Anda berencana untuk memperluas fungsionalitas? Saya pikir tidak ada praktik terbaik umum - solusi tergantung pada jawaban dari pertanyaan-pertanyaan ini. Dan itu mulai menjadi masalah bahkan dalam model sederhana seperti ini. Atau Anda mungkin berakhir dengan model yang mengikuti beberapa prinsip yang lebih tinggi tetapi benar-benar menyebalkan dalam skenario paling penting - yang dilihat oleh pengguna sistem.
Štefan Oravec
Poin bagus! Saya harus meluangkan waktu memikirkan hal ini!
matikin9

Jawaban:

6

Masalah dengan contoh pertama Anda adalah tabel tri-link. Apakah itu akan memerlukan salah satu kunci asing pada laporan atau rekomendasi untuk selalu NULL sehingga kata kunci hanya menghubungkan satu cara atau yang lain?

Dalam kasus contoh kedua Anda, bergabung dari basis ke tabel turunan sekarang mungkin memerlukan penggunaan pemilih jenis atau GABUNGAN KIRI tergantung pada bagaimana Anda melakukannya.

Mengingat itu, mengapa tidak membuatnya eksplisit dan menghilangkan semua NULL dan KIRI GABUNG?

Reports
----------
ReportID
ReportName


Recommendations
----------
RecommendationID
RecommendationName
ReportID (foreign key)


Keywords
----------
KeywordID
KeywordName


ReportKeywords
----------
KeywordID (foreign key)
ReportID (foreign key)

RecommendationKeywords
----------
KeywordID (foreign key)
RecommendationID (foreign key)

Dalam skenario ini saat Anda menambahkan sesuatu yang perlu ditandai, Anda cukup menambahkan tabel entitas dan tabel tautan.

Kemudian hasil pencarian Anda terlihat seperti ini (lihat masih ada pemilihan jenis yang terjadi dan mengubahnya menjadi generik pada tingkat hasil objek jika Anda menginginkan daftar hasil tunggal):

SELECT CAST('REPORT' AS VARCHAR(15)) AS ResultType
    ,Reports.ReportID AS ObjectID
    ,Reports.ReportName AS ObjectName
FROM Keywords
INNER JOIN ReportKeywords
    ON ReportKeywords.KeywordID = Keywords.KeywordID
INNER JOIN Reports
    ON Reports.ReportID = ReportKeywords.ReportID
WHERE Keywords.KeywordName LIKE '%' + @SearchCriteria + '%'
UNION ALL
SELECT 'RECOMMENDATION' AS ResultType
    ,Recommendations.RecommendationID AS ObjectID
    ,Recommendations.RecommendationName AS ObjectName
FROM Keywords
INNER JOIN RecommendationKeywords
    ON RecommendationKeywords.KeywordID = Keywords.KeywordID
INNER JOIN Recommendations
    ON Recommendations.RecommendationID = RecommendationKeywords.ReportID
WHERE Keywords.KeywordName LIKE '%' + @SearchCriteria + '%'

Tidak peduli apa, di suatu tempat akan ada pemilihan jenis dan semacam percabangan terjadi.

Jika Anda melihat bagaimana Anda akan melakukan ini dalam opsi 1 Anda, itu serupa tetapi dengan pernyataan KASUS atau GABUNG KIRI dan COALESCE. Saat Anda memperluas opsi 2 dengan lebih banyak hal yang ditautkan, Anda harus terus menambahkan lebih banyak GABUNG KIRI di mana hal-hal yang biasanya TIDAK ditemukan (objek yang ditautkan hanya dapat memiliki satu tabel turunan yang valid).

Saya tidak berpikir ada sesuatu yang secara fundamental salah dengan pilihan Anda 2, dan Anda benar-benar bisa membuatnya terlihat seperti proposal ini dengan menggunakan pandangan.

Dalam opsi Anda 1, saya mengalami kesulitan melihat mengapa Anda memilih tabel tri-link.

Cade Roux
sumber
Tabel tri-tautan yang Anda sebutkan mungkin adalah akibat dari saya secara mental malas ...: P Setelah membaca berbagai jawaban, saya pikir tidak satu pun dari pilihan awal saya masuk akal. Memiliki tabel ReportKeywords dan RecommendationKeywords yang terpisah menjadi lebih praktis. Saya sedang mempertimbangkan skalabilitas, dalam hal berpotensi memiliki lebih banyak objek yang membutuhkan kata kunci yang diterapkan, tetapi secara realistis mungkin hanya ada satu jenis objek lagi yang dapat membutuhkan kata kunci.
matikin9
4

Pertama, perhatikan bahwa solusi ideal tergantung sampai batas tertentu pada RDBMS yang Anda gunakan. Saya akan memberikan jawaban standar dan spesifik PostgreSQL.

Normalisasi, Jawaban Standar

Jawaban standar adalah memiliki dua tabel bergabung.

Misalkan kita memiliki tabel kita:

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)
);

Pendekatan ini mengikuti semua aturan normalisasi standar, dan tidak melanggar prinsip normalisasi database tradisional. Ini harus bekerja pada RDBMS apa pun.

Jawaban spesifik PostgreSQL, desain N1NF

Pertama, kata mengapa PostgreSQL berbeda. PostgreSQL mendukung sejumlah cara yang sangat berguna untuk menggunakan indeks di atas array, terutama menggunakan apa yang dikenal sebagai indeks GIN. Ini dapat menguntungkan kinerja sedikit jika digunakan dengan benar di sini. Karena PostgreSQL dapat "menjangkau" tipe data dengan cara ini, asumsi dasar atomisitas dan normalisasi agak bermasalah untuk diterapkan secara kaku di sini. Jadi untuk alasan ini, rekomendasi saya adalah untuk melanggar aturan atomisitas bentuk normal pertama dan mengandalkan indeks GIN untuk kinerja yang lebih baik.

Catatan kedua di sini adalah bahwa sementara ini memberikan kinerja yang lebih baik, ini menambah beberapa sakit kepala karena Anda akan memiliki beberapa pekerjaan manual yang harus dilakukan untuk mendapatkan integritas referensial untuk bekerja dengan benar. Jadi tradeoff di sini adalah kinerja untuk pekerjaan manual.

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[]  
);

Sekarang kita harus menambahkan pemicu untuk memastikan bahwa kata kunci dikelola dengan benar.

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();

Kedua, kita harus memutuskan apa yang harus dilakukan ketika kata kunci dihapus. Seperti yang ada sekarang, kata kunci yang dihapus dari tabel kata kunci tidak akan mengalir ke bidang kata kunci. Mungkin ini diinginkan dan mungkin juga tidak. Hal paling sederhana yang harus dilakukan adalah batasi saja penghapusan selalu dan berharap Anda akan menangani kasus ini secara manual jika muncul (gunakan pemicu untuk keamanan di sini). Opsi lain mungkin menulis ulang setiap nilai kata kunci di mana kata kunci itu ada untuk menghapusnya. Sekali lagi pemicu akan menjadi cara untuk melakukan itu juga.

Keuntungan besar dari solusi ini adalah Anda dapat mengindeks untuk pencarian sangat cepat berdasarkan kata kunci, dan Anda dapat menarik semua tag tanpa bergabung. Kerugiannya adalah bahwa menghapus kata kunci itu menyakitkan, dan tidak akan berkinerja baik bahkan pada hari yang baik. Ini mungkin dapat diterima karena ini adalah peristiwa langka dan dapat dikirim ke proses latar belakang tetapi merupakan tradeoff yang layak dipahami.

Mengkritik Solusi Pertama Anda

Masalah sebenarnya dengan solusi pertama Anda adalah bahwa Anda tidak memiliki kunci yang mungkin pada ObjectKeywords. Akibatnya Anda memiliki masalah di mana Anda tidak dapat menjamin bahwa setiap kata kunci diterapkan untuk setiap objek hanya sekali.

Solusi kedua Anda sedikit lebih baik. Jika Anda tidak menyukai solusi lain yang ditawarkan, saya sarankan menggunakannya. Namun saya menyarankan untuk menyingkirkan keyword_id dan hanya bergabung pada teks kata kunci. Itu menghilangkan bergabung tanpa denormalisasi.

Chris Travers
sumber
Saya menggunakan MS SQL Server untuk proyek ini, tetapi terima kasih atas info tentang PostgreSQL. Poin lain yang Anda bawa tentang menghapus dan memastikan pasangan objek-kata kunci masing-masing hanya terjadi sekali. Meskipun saya memiliki kunci untuk setiap pasangan objek-kata kunci, bukankah saya masih harus memeriksa sebelum memasukkan? Adapun memiliki kata kunci id terpisah ... Saya membaca bahwa untuk SQL Server, memiliki string panjang-ish dapat mengurangi kinerja, dan saya mungkin harus mengizinkan pengguna untuk memasukkan "frase kunci" daripada hanya "kata kunci" ".
matikin9
0

Saya akan menyarankan dua struktur terpisah:

report_keywords
---------------
  ID laporan
  ID kata kunci

Recommend_keywords
-----------------------
  rekomendasi_id
  kata kunci_id

Dengan cara ini Anda tidak memiliki semua id entitas yang mungkin dalam tabel yang sama (yang tidak terlalu skalabel, dan bisa membingungkan), dan Anda tidak memiliki tabel dengan "id objek" generik yang harus disambiguasi di tempat lain menggunakan base_objecttabel, yang akan bekerja, tapi saya pikir desainnya terlalu rumit.

FrustratedWithFormsDesigner
sumber
Saya tidak setuju bahwa apa yang Anda sarankan adalah pilihan yang layak tetapi mengapa RI tidak dapat ditegakkan dengan desain B dari OP? (Saya menganggap itu yang Anda katakan).
ypercubeᵀᴹ
@ ypercube: Saya pikir saya melewatkan BaseObjectstabel pada read-through pertama saya, dan saya pikir saya melihat deskripsi untuk tabel di mana object_iddapat menunjuk ke ID di tabel mana pun .
FrustratedWithFormsDesigner
-1

Dalam pengalaman saya, inilah yang dapat Anda lakukan.

Reports
----------
Report_id (primary_key)
Report_name

Recommendations
----------------
Recommendation_id (primary key)
Recommendation_name
Report_id (foreign key)

Keywords
----------
Keyword_id (primary key)
Keyword

Dan untuk hubungan antara kata kunci, laporan, dan rekomendasi, Anda dapat melakukan salah satu dari dua opsi: Opsi A:

Recommendation_keywords
------------------------
Recommendation_id(foreign_key)
keyword_id (foreign_key)

Ini memungkinkan hubungan langsung dari Laporan ke Rekomendasi, ke Kata Kunci dan akhirnya ke Kata Kunci. Opsi B:

object_keywords
---------------
Object_id
Object_type
Keyword_id(foreign_key)

Opsi A adalah yang lebih mudah untuk diterapkan dan dikelola karena akan memiliki constratints dari database untuk menangani integritas data dan tidak akan membiarkan penyisipan data yang tidak valid.

Opsi B meskipun memerlukan sedikit lebih banyak pekerjaan karena Anda perlu kode identifikasi hubungan. Lebih fleksibel dalam jangka panjang, jika kebetulan di beberapa titik di masa depan Anda perlu menambahkan kata kunci ke item lain selain laporan atau rekomendasi Anda hanya perlu menambahkan identifikasi dan menggunakan langsung tabel.

Erxgli
sumber
Izinkan saya menjelaskan mengapa saya menurunkan suara: 1. Tidak jelas apakah Anda mendukung opsi A, B atau pendekatan ke-3. Tampaknya (bagi saya) bahwa Anda mengatakan bahwa keduanya kurang lebih baik (dengan yang saya tidak setuju karena A memiliki beberapa masalah yang dijabarkan orang lain dengan jawaban mereka. 2. Apakah Anda menyarankan untuk melakukan perbaikan pada desain A (atau B) "Tidak jelas juga. Akan baik juga jika mendefinisikan FK dengan jelas, sama sekali tidak jelas apa yang Anda sarankan. Secara total saya suka jawaban yang mengklarifikasi hal-hal dan opsi untuk pengunjung masa depan. Silakan coba edit jawaban Anda dan Saya akan membalikkan suara saya
ypercubeᵀᴹ