Normalization និងការរចនា Schema
មាតិកា
- អន្ទាក់ Table មួយធំ
- Anomaly ទាំងបី
- គំនិតនៅពីក្រោយ Normalization
- Normal Form ទី 1៖ មួយតម្លៃក្នុងមួយ Cell
- Normal Form ទី 2៖ ពឹងលើ Key ទាំងមូល
- Normal Form ទី 3៖ គ្មានអ្វីក្រៅពី Key
- ពិនិត្យ shopdb
- ហេតុអ្វី unit_price ជាច្បាប់ចម្លងដោយចេតនា
- Many-to-Many និងអនុសញ្ញាដាក់ឈ្មោះ
- ការជ្រើសរើស Data Type ឱ្យបានល្អ
- Soft Delete និងពេលណាត្រូវ Denormalize
- បញ្ជីត្រួតពិនិត្យការរចនា
- Migration៖ ឯកសារ SQL ដែលមានលេខ
- សាកល្បងដោយខ្លួនឯង
1. អន្ទាក់ Table មួយធំ
អ្នកគ្រប់គ្នាដែលមកពី spreadsheet រចនា table ដំបូងរបស់ខ្លួនតាមរបៀបដូចគ្នា៖ ដាក់អ្វីៗទាំងអស់នៅកន្លែងតែមួយ។ order មានអតិថិជន អតិថិជនមានទីក្រុង order មានផលិតផល ហើយផលិតផលមានតម្លៃ។ ដូច្នេះហេតុអ្វីមិនធ្វើ table តែមួយដែលមាន column សម្រាប់អ្វីៗទាំងនោះ?
នេះគឺជាការរចនានោះ ក្នុងទម្រង់ជា scratch table (យើងនឹងមិនប៉ះ shopdb ក្នុងផ្នែកនេះទេ)។ ទិន្នន័យគឺជា order បីដំបូងនៃហាងរបស់យើង៖
CREATE TABLE orders_flat (
order_id integer,
customer_name text,
customer_email text,
customer_city text,
product_name text,
product_price numeric(10,2),
quantity integer
);
INSERT INTO orders_flat VALUES
(1, 'Sokha Chan', 'sokha@example.com', 'Phnom Penh', 'Wireless Mouse', 15.00, 1),
(1, 'Sokha Chan', 'sokha@example.com', 'Phnom Penh', 'USB-C Cable', 5.50, 2),
(2, 'Sokha Chan', 'sokha@example.com', 'Phnom Penh', 'Mechanical Keyboard', 79.00, 1),
(3, 'Dara Kim', 'dara@example.com', 'Siem Reap', 'SQL Cookbook', 32.00, 1),
(3, 'Dara Kim', 'dara@example.com', 'Siem Reap', 'Clean Code', 28.50, 1);
order_id | customer_name | customer_email | customer_city | product_name | product_price | quantity
----------+---------------+-------------------+---------------+---------------------+---------------+---------
1 | Sokha Chan | sokha@example.com | Phnom Penh | Wireless Mouse | 15.00 | 1
1 | Sokha Chan | sokha@example.com | Phnom Penh | USB-C Cable | 5.50 | 2
2 | Sokha Chan | sokha@example.com | Phnom Penh | Mechanical Keyboard | 79.00 | 1
3 | Dara Kim | dara@example.com | Siem Reap | SQL Cookbook | 32.00 | 1
3 | Dara Kim | dara@example.com | Siem Reap | Clean Code | 28.50 | 1
វាដំណើរការសម្រាប់ការបង្ហាញ (demo)។ សូមកត់សម្គាល់ថាមានអ្វីច្រើនដែលស្ទួន៖ ឈ្មោះ អ៊ីមែល និងទីក្រុងរបស់ Sokha លេចឡើងបីដង។ តម្លៃផលិតផលនីមួយៗត្រូវបានសរសេរលើគ្រប់ row ដែលនិយាយអំពីវា។ ការស្ទួននោះហៅថា redundancy ហើយវាជាកន្លែងដែលបញ្ហាចាប់ផ្តើម។
2. Anomaly ទាំងបី
Redundancy បង្កឱ្យមាន bug បីប្រភេទ ហៅថា anomaly។ នីមួយៗគឺជាវិធីដែល table អាចកុហកបាន។
Update anomaly។ Sokha ផ្លាស់ទៅ Siem Reap។ អ្នកត្រូវផ្លាស់ប្តូរទីក្រុងលើគ្រប់ row ដែលនិយាយអំពី Sokha៖
UPDATE orders_flat
SET customer_city = 'Siem Reap'
WHERE order_id = 1;
អ្នកបាន update order 1 ហើយភ្លេច order 2។ ឥឡូវនេះអតិថិជនតែម្នាក់រស់នៅពីរទីក្រុង ហើយគ្មានច្បាប់ណាក្នុងមូលដ្ឋានទិន្នន័យប្រាប់ថាមួយណាពិតទេ។
Insert anomaly។ អតិថិជនថ្មីម្នាក់ឈ្មោះ Vuthy ចុះឈ្មោះ ប៉ុន្តែមិនទាន់បានបញ្ជាទិញនៅឡើយ។ តើអ្នករក្សាទុកគាត់នៅទីណា? គ្រប់ row ត្រូវការ order_id និងផលិតផល។ អ្នកមិនអាចកត់ត្រាអតិថិជនដែលគ្មាន order បានទេ លុះត្រាតែអ្នកបំពេញ row ដោយ NULL និងទិន្នន័យក្លែងក្លាយ។ ដូចគ្នាដែរសម្រាប់ផលិតផលថ្មីដែលគ្មាននរណាទិញ។
Delete anomaly។ Dara លុបចោលការបញ្ជាទិញ ហើយអ្នកលុប order 3 ដើម្បីសម្អាត៖
DELETE FROM orders_flat WHERE order_id = 3;
អ៊ីមែល និងទីក្រុងរបស់ Dara បាត់ទៅដែរ រួមជាមួយការពិតថា "SQL Cookbook" មានតម្លៃ 32.00។ អ្នកចង់លុប order មួយ ប៉ុន្តែបានលុបអតិថិជនមួយនាក់ និងផលិតផលពីរដោយចៃដន្យ។
| Anomaly | អ្វីដែលខុស | មូលហេតុដើម |
|---|---|---|
| Update | ការពិតតែមួយដែលរក្សាទុកពីរកន្លែងមិនត្រូវគ្នា | ការពិតមួយត្រូវបានស្ទួន |
| Insert | មិនអាចរក្សាទុកការពិតមួយដោយគ្មានការពិតមួយទៀតដែលមិនទាក់ទងគ្នា | ការពិតអំពីវត្ថុផ្សេងគ្នារួមគ្នាក្នុង row តែមួយ |
| Delete | ការលុបការពិតមួយបំផ្លាញការពិតមួយទៀត | ការពិតអំពីវត្ថុផ្សេងគ្នារួមគ្នាក្នុង row តែមួយ |
សម្អាតនៅពេលអ្នកធ្វើរួច៖
DROP TABLE orders_flat;
3. គំនិតនៅពីក្រោយ Normalization
Normalization គឺជាដំណើរការនៃការបំបែក table ដើម្បីឱ្យការពិតនីមួយៗត្រូវបានរក្សាទុកនៅត្រឹមកន្លែងតែមួយប៉ុណ្ណោះ។ ប្រសិនបើទីក្រុងរបស់អតិថិជនត្រូវបានរក្សាទុកតែម្តង វានឹងមិនអាចមិនត្រូវគ្នានឹងខ្លួនវាបានទេ។ ប្រសិនបើអតិថិជនរស់នៅក្នុង table ផ្ទាល់ខ្លួន អ្នកអាចបន្ថែមម្នាក់ដែលគ្មាន order ហើយលុប order មួយដោយមិនបាត់អតិថិជននោះ។
អ្នកបានធ្វើរឿងនេះរួចហើយនៅពេលអ្នកសាងសង់ shopdb។ flat table ខាងលើបានក្លាយជា customers, products, orders និង order_items ដែលភ្ជាប់គ្នាដោយ key។ ទ្រឹស្តីនៅពីក្រោយវាគឺជាសំណុំច្បាប់ហៅថា normal form ដែលដាក់លេខ 1NF, 2NF, 3NF និងបន្តទៀត។ ទម្រង់នីមួយៗសាងសង់លើទម្រង់មុន។ សម្រាប់ការងារប្រចាំថ្ងៃ 3NF គឺជាគោលដៅ ហើយមានសេចក្តីសង្ខេបងាយចាំអំពីវា ដែលយើងនឹងជួបក្នុងផ្នែកទី 6។
កុំទន្ទេញនិយមន័យផ្លូវការ។ រៀនសួរសំណួរមួយអំពីគ្រប់ column ថា៖ "តើនេះជាការពិតអំពីវត្ថុដែល row នេះតំណាងឬទេ?"
4. Normal Form ទី 1៖ មួយតម្លៃក្នុងមួយ Cell
1NF និយាយថា៖ គ្រប់ cell ផ្ទុកតម្លៃតែមួយ ហើយគ្មានក្រុមដែលកើតឡើងម្តងទៀត (repeating group)។ និយាយម្យ៉ាងទៀត គ្មានបញ្ជីត្រូវបានលាយបញ្ចូលក្នុង column ទេ។
ការបំពានដែលគួរឱ្យទាក់ទាញគឺការរក្សាទុកធាតុនៃ order ក្នុង cell តែមួយ៖
order_id | customer | products
----------+----------+--------------------------------
1 | Sokha | Wireless Mouse, USB-C Cable
3 | Dara | SQL Cookbook, Clean Code
ឥឡូវសាកល្បងឆ្លើយថា "តើមាន order ប៉ុន្មានដែលមាន USB-C Cable?" អ្នកត្រូវញែកអត្ថបទដោយ LIKE '%USB-C Cable%'។ សាកល្បងថា "តើតម្លៃរបស់ផលិតផលទីពីរប៉ុន្មាន?" គ្មានវិធីសួរទេ។ សាកល្បងបន្ថែមផលិតផល៖ អ្នកកែ string។ មូលដ្ឋានទិន្នន័យមិនអាចអនុវត្ត foreign key លើបំណែកមួយនៃ string បានទេ។
ការបំពានមួយទៀតគឺ column ដែលដាក់លេខ៖ product1, product2, product3។ តើមានអ្វីកើតឡើងជាមួយផលិតផលទីបួន? អ្នកបន្ថែម column ហើយសរសេរ query ទាំងអស់ឡើងវិញ។
ដំណោះស្រាយគឺដូចគ្នាក្នុងករណីទាំងពីរ៖ មួយ row ក្នុងមួយធាតុ។ នោះពិតជាអ្វីដែល order_items គឺ។ order ដែលមានផលិតផលបីមាន row បីនៅទីនោះ ច្រើនតាមដែលវាត្រូវការ ហើយនីមួយៗអាច JOIN រាប់ និងដាក់ constraint បាន។
ការសាកល្បងរហ័សសម្រាប់ 1NF៖ ប្រសិនបើអ្នកធ្លាប់ចង់សរសេរ split(',') លើ column មួយ នោះការរចនាខុសហើយ។
5. Normal Form ទី 2៖ ពឹងលើ Key ទាំងមូល
2NF មានសារៈសំខាន់តែនៅពេល table មាន composite primary key (ពីរ column ឬច្រើនជាងនេះ)។ វានិយាយថា៖ គ្រប់ column ដែលមិនមែនជា key ត្រូវពឹងលើ key ទាំងមូល មិនមែនគ្រាន់តែផ្នែកមួយនៃវាទេ។
មើល orders_flat ម្តងទៀត។ តើអ្វីកំណត់អត្តសញ្ញាណ row? គូ (order_id, product_name)។ ឥឡូវសួរអំពី column ផ្សេងទៀតនីមួយៗ៖
quantity៖ ពឹងលើទាំងពីរ។ វាជាចំនួន ផលិតផលនេះ នៅក្នុង order នេះ។ ល្អ។customer_name៖ ពឹងលើតែorder_idប៉ុណ្ណោះ។ ការដឹង order ប្រាប់អ្នកអំពីអតិថិជន មិនថាផលិតផលអ្វីឡើយ។ នោះគឺជា partial dependency។product_price៖ ពឹងលើតែproduct_nameប៉ុណ្ណោះ។ order មិនពាក់ព័ន្ធ។ partial dependency មួយទៀត។
Partial dependency គឺជាប្រភពនៃការស្ទួនពីផ្នែកទី 1។ ដំណោះស្រាយគឺផ្លាស់ទីការពិតនីមួយៗទៅ table ដែលមាន key តាមអ្វីដែលវាពឹងលើពិតប្រាកដ៖
| Column | ពឹងលើ | ស្ថិតនៅក្នុង |
|---|---|---|
customer_name | order (តាមពិតគឺអតិថិជន) | customers / orders |
product_price | ផលិតផល | products |
quantity | order និងផលិតផល | order_items |
order_items របស់យើងមាន composite key (order_id, product_id) ហើយ column ផ្សេងទៀតតែមួយគត់របស់វាគឺ quantity និង unit_price ដែលជារបស់គូជាក់លាក់នោះ។ វាឆ្លងកាត់ 2NF។ (យើងនឹងមើល unit_price ឱ្យលម្អិតជាងនេះបន្តិចទៀត។)
6. Normal Form ទី 3៖ គ្មានអ្វីក្រៅពី Key
3NF និយាយថា៖ column ដែលមិនមែនជា key ត្រូវពឹងលើ key ហើយ មិនមែនពឹងលើ column ផ្សេងទៀតដែលមិនមែនជា key ឡើយ។ នេះគឺជា transitive dependency ហើយងាយខកខានមើលឆ្លង។
ការរចនាដ៏គួរឱ្យទាក់ទាញមួយសម្រាប់ orders គឺបន្ថែមទីក្រុងរបស់អតិថិជនសម្រាប់របាយការណ៍លឿនជាង៖
orders
id | customer_id | customer_city | status
----+-------------+---------------+--------
1 | 1 | Phnom Penh | paid
customer_city ពឹងលើ customer_id ដែលជា column មិនមែនជា key របស់ orders។ ខ្សែសង្វាក់គឺ orders.id -> customer_id -> city។ ទីក្រុងគឺជាការពិតអំពីអតិថិជន មិនមែនអំពី order ទេ ដូច្នេះវាជារបស់ customers ដែលវាត្រូវបានរក្សាទុកតែម្តង។ ប្រសិនបើអ្នកបំពាន 3NF អ្នកនឹងទទួលបាន update anomaly ម្តងទៀត៖ ផ្លាស់ប្តូរទីក្រុងរបស់អតិថិជន ហើយ order ចាស់ៗមិនត្រូវគ្នា។
សេចក្តីសង្ខេបមួយបន្ទាត់ដ៏ល្បីអំពីទម្រង់បីដំបូង៖ គ្រប់ column ដែលមិនមែនជា key ត្រូវផ្តល់ការពិតអំពី key, key ទាំងមូល និងគ្មានអ្វីក្រៅពី key។
នេះគឺជា table សម្រាប់ចងចាំ៖
| Form | ច្បាប់ជាភាសាសាមញ្ញ | ការបំពានមើលទៅដូចជា |
|---|---|---|
| 1NF | មួយតម្លៃក្នុងមួយ cell គ្មាន repeating group | "a, b, c" ក្នុង cell, column item1, item2 |
| 2NF | ពឹងលើ key ទាំងមូល | ឈ្មោះអតិថិជនក្នុង table ដែលមាន key តាម order និងផលិតផល |
| 3NF | មិនពឹងលើអ្វីក្រៅពី key | ទីក្រុងរក្សាទុកក្នុង orders ក្បែរ customer_id |
7. ពិនិត្យ shopdb
តោះត្រួតពិនិត្យ schema របស់យើងពីជំពូកទី 6 ម្តងមួយ table។ ភ្ជាប់ទៅមូលដ្ឋានទិន្នន័យ ហើយមើលនីមួយៗដោយ \d៖
shopdb=# \d products
Table "public.products"
Column | Type | Collation | Nullable | Default
-------------+---------------+-----------+----------+------------------------------
id | integer | | not null | generated always as identity
name | text | | not null |
price | numeric(10,2) | | not null |
stock | integer | | not null | 0
category_id | integer | | |
(លទ្ធផលធម្មតា ការដាក់ដកឃ្លារបស់អ្នកអាចខុសបន្តិចបន្តួច។)
| Table | សេចក្តីសម្រេច |
|---|---|
customers | គ្រប់ column ជាការពិតអំពីអតិថិជន។ ល្អ |
categories | មួយការពិតក្នុងមួយ row។ ល្អ។ ឈ្មោះ category ស្ថិតនៅកន្លែងតែមួយ មិនស្ទួនលើគ្រប់ផលិតផលទេ |
products | តម្លៃ និងស្តុកពិពណ៌នាអំពីផលិតផល។ category_id ចង្អុលទៅ category ជាជាងស្ទួនឈ្មោះរបស់វា។ ល្អ |
orders | ការពិតអំពី order៖ អ្នកណា ស្ថានភាព ពេលណា។ ព័ត៌មានលម្អិតរបស់អតិថិជននៅក្នុង customers។ ល្អ |
order_items | ការពិតអំពីផលិតផលមួយក្នុង order មួយ។ ល្អ ជាមួយ column មួយដែលល្អិតល្អន់ |
column ដែលល្អិតល្អន់នោះគឺ unit_price។ វាមើលទៅដូចជាការបំពាន 3NF ព្រោះតម្លៃមានរួចហើយក្នុង products។ តោះមើលវា។
8. ហេតុអ្វី unit_price ជាច្បាប់ចម្លងដោយចេតនា
order_items.unit_price ស្ទួនព័ត៌មានពី products.price។ តាមច្បាប់ខាងលើ នោះគឺជា redundancy។ ហេតុអ្វីយើងធ្វើដូច្នេះ?
ព្រោះតម្លៃទាំងពីរគឺជា ការពិតខុសគ្នា៖
products.priceឆ្លើយសំណួរ "តើវាមានតម្លៃប៉ុន្មាន ឥឡូវនេះ?"order_items.unit_priceឆ្លើយសំណួរ "តើអតិថិជន បានបង់ ប៉ុន្មាននៅពេលទិញ?"
ស្រមៃថាអ្នកលុប column នោះ ហើយ JOIN ទៅ products.price ជានិច្ច។ នៅខែមីនា អ្នកដំឡើងតម្លៃ Wireless Mouse ពី 15.00 ទៅ 18.00។ គ្រប់ order កាលពីមុនសម្រាប់ mouse ឥឡូវមើលទៅដូចជាត្រូវបានលក់ក្នុងតម្លៃ 18.00។ របាយការណ៍ចំណូលខែមករាផ្លាស់ប្តូរដោយអតីតកាល វិក្កយបត្រ (invoice) ដែលអ្នកបានផ្ញើរួចលែងត្រូវគ្នា ហើយគណនេយ្យករមិនសប្បាយចិត្ត។
អ្នកអាចឃើញភាពខុសគ្នាក្នុង query តែមួយ៖
-- Update តម្លៃបច្ចុប្បន្ន។ order កាលពីមុនរក្សាតម្លៃដែលបានបង់ពិតប្រាកដ។
UPDATE products SET price = 18.00 WHERE id = 1;
SELECT p.name,
p.price AS price_now,
oi.unit_price AS price_paid
FROM order_items oi
JOIN products p ON p.id = oi.product_id
WHERE oi.product_id = 1;
name | price_now | price_paid
----------------+-----------+------------
Wireless Mouse | 18.00 | 15.00
Wireless Mouse | 18.00 | 15.00
(2 rows)
(row ទាំងពីរគឺ order 1 និង order 4។) ដាក់តម្លៃត្រឡប់មកវិញ ដើម្បីឱ្យជំពូកក្រោយៗត្រូវគ្នា៖
UPDATE products SET price = 15.00 WHERE id = 1;
នេះគឺជា snapshot៖ តម្លៃដែលបានចម្លងនៅពេលណាមួយ ព្រោះវា មិនគួរ ផ្លាស់ប្តូរនៅពេលដើមផ្លាស់ប្តូរ។ គំនិតដូចគ្នាអនុវត្តចំពោះអាសយដ្ឋានដឹកជញ្ជូនលើ order អត្រាពន្ធ ឬឈ្មោះផលិតផលលើ invoice។ ក្បួនដៃ៖ ប្រសិនបើច្បាប់ចម្លងត្រូវតែនៅដដែលនៅពេលប្រភពផ្លាស់ប្តូរ នោះវាជាប្រវត្តិសាស្ត្រ មិនមែន redundancy ទេ។ ប្រសិនបើច្បាប់ចម្លងត្រូវតាមប្រភព នោះវាជា bug ដែលកំពុងរង់ចាំកើតឡើង។
9. Many-to-Many និងអនុសញ្ញាដាក់ឈ្មោះ
អ្នកបានជួប many-to-many នៅជំពូកទី 6៖ order មានផលិតផលច្រើន ហើយផលិតផលមួយលេចឡើងក្នុង order ច្រើន។ មូលដ្ឋានទិន្នន័យ relational មិនអាចរក្សាទុកវាដោយផ្ទាល់ក្នុង table ណាមួយបានទេ ដូច្នេះអ្នកបន្ថែម table ទីបីនៅកណ្តាល (junction table ហៅម្យ៉ាងទៀតថា join ឬ link table)។ order_items គឺជាមួយ។ composite primary key (order_id, product_id) របស់វាក៏ការពារមិនឱ្យផលិតផលតែមួយលេចឡើងពីរដងក្នុង order តែមួយដែរ។
ប្រសិនបើ junction មិនត្រូវការ column បន្ថែម វាអាចជាគ្រាន់តែ foreign key ពីរ។ នេះជាឧទាហរណ៍ scratch សម្រាប់ដាក់ tag លើផលិតផល៖
CREATE TABLE tags (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL UNIQUE
);
CREATE TABLE product_tags (
product_id integer REFERENCES products(id) ON DELETE CASCADE,
tag_id integer REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (product_id, tag_id)
);
ឈ្មោះក៏ជាផ្នែកមួយនៃការរចនាដែរ។ អនុសញ្ញាទាំងនេះធ្វើឱ្យ schema អាចទាយទុកបាន ហើយ shopdb អនុវត្តតាមរួចហើយ៖
| អនុសញ្ញា | ឧទាហរណ៍ | ហេតុផល |
|---|---|---|
| ឈ្មោះ table ជាពហុវចនៈ | customers, orders | table គឺជាបណ្តុំ |
| ឈ្មោះ column ជាឯកវចនៈ | name, status | column គឺជាការពិតមួយ |
snake_case អក្សរតូច | created_at | ជៀសវាងបញ្ហា quoting ក្នុង PostgreSQL |
Primary key ជា id | customers.id | ឈ្មោះដូចគ្នាជានិច្ច |
Foreign key ជា <table>_id | customer_id | អានថា "ចង្អុលទៅអតិថិជន" |
Timestamp បញ្ចប់ដោយ _at | ordered_at | ប្រភេទច្បាស់ |
| Boolean អានជាសំណួរ | is_active | WHERE is_active អានដូចភាសាអង់គ្លេស |
PostgreSQL បំប្លែងឈ្មោះដែលគ្មាន quote ទៅជាអក្សរតូច ដូច្នេះ CustomerName ក្លាយជា customername។ អក្សរធំលាយអក្សរតូចបង្ខំអ្នកឱ្យ quote ជារៀងរហូត។ ប្រកាន់ snake_case ហើយមិនបាច់គិតអំពីវាទៀត។
ជ្រើសរើសអនុសញ្ញាមួយ ហើយរក្សាវា។ ភាពស្របគ្នាសំខាន់ជាងការដែលអ្នកជ្រើសមួយណា។
10. ការជ្រើសរើស Data Type ឱ្យបានល្អ
type របស់ column ក៏ជា constraint មួយដែរ។ សូមពិនិត្យឡើងវិញនូវជម្រើសពីជំពូកទី 3 ដោយគិតអំពីការរចនា៖
| ទិន្នន័យ | ប្រើ | ជៀសវាង | ហេតុផល |
|---|---|---|---|
| ប្រាក់ | numeric(10,2) | real, double precision | Float មិនអាចរក្សាទុក 0.10 បានត្រឹមត្រូវទេ |
| Timestamp | timestamptz | timestamp, text | រក្សាទុកពេលវេលាច្បាស់លាស់ ហើយបំប្លែងតាមល្វែងម៉ោងនៃ session |
| កាលបរិច្ឆេទតែប៉ុណ្ណោះ | date | text '2026-01-05' | មានការគណនាកាលបរិច្ឆេទ និង validation ឥតគិតថ្លៃ |
| បាទ/ចាស ឬទេ | boolean | integer 0/1, 'Y'/'N' | ច្បាស់លាស់ និងមានការត្រួតពិនិត្យ |
| ជម្រើសថេរ | text + CHECK ... IN (...) | free text | កំហុសអក្ខរាវិរុទ្ធដូចជា 'shiped' ត្រូវបានបដិសេធ |
| ឈ្មោះ អ៊ីមែល | text | varchar(50) ដែលស្មាន | Postgres គ្មានទណ្ឌកម្មល្បឿន ហើយអ្នកជៀសវាង bug ការកាត់ខ្លី (truncation) |
| Surrogate key | integer GENERATED ALWAYS AS IDENTITY | serial សម្រាប់កូដថ្មី | SQL ស្តង់ដារ ពិបាកសរសេរជាន់ដោយចៃដន្យ |
| ID សាធារណៈ | uuid | id តាមលំដាប់ក្នុង URL | មិនអាចទាយបាន។ ស្រេចចិត្ត |
ទម្លាប់ពីរបន្ថែមទៀត។ កំណត់ column ជា NOT NULL លុះត្រាតែ "មិនស្គាល់" ជាស្ថានភាពពិតប្រាកដ និងមានអត្ថន័យ (ដូចជាទីក្រុងដែលបាត់)។ ហើយសរសេរ CHECK constraint។ constraint ក្នុងមូលដ្ឋានទិន្នន័យការពារគ្រប់កម្មវិធីដែលប៉ះវា មិនមែនតែកម្មវិធីដែលអ្នកបានសរសេរថ្ងៃនេះទេ។
11. Soft Delete និងពេលណាត្រូវ Denormalize
Soft delete។ ពេលខ្លះអ្នកមិនចង់ឱ្យ row បាត់ទេ។ លំនាំទូទៅបន្ថែម column deleted_at timestamptz។ "ការលុប" កំណត់វាទៅ now() ហើយគ្រប់ query បន្ថែម WHERE deleted_at IS NULL៖
ALTER TABLE products ADD COLUMN deleted_at timestamptz;
UPDATE products SET deleted_at = now() WHERE id = 6; -- soft delete
SELECT * FROM products WHERE deleted_at IS NULL; -- តែធាតុសកម្ម
វារក្សាប្រវត្តិ និងធ្វើឱ្យកំហុសអាចត្រឡប់វិញបាន។ ថ្លៃដើមក៏ពិតដែរ៖ គ្រប់ query ត្រូវចាំ filter (view ជួយបាន) unique constraint ក្លាយជាស្មុគស្មាញ (អ្នកប្រើ "ត្រូវបានលុប" ពីរនាក់អាចមានអ៊ីមែលដូចគ្នា) ហើយទិន្នន័យផ្ទាល់ខ្លួនដែលអ្នកបានសន្យាថានឹងលុបនៅតែមាន។ ប្រើវានៅពេលអ្នកត្រូវការប្រវត្តិ មិនមែនជាលំនាំដើមទេ។ (order របស់យើងជៀសវាងការលុបរួចហើយតាមរបៀបផ្សេង៖ ស្ថានភាព cancelled។)
ប្រសិនបើអ្នកបានសាកល្បងឧទាហរណ៍នោះ សូមមិនធ្វើវិញដោយ៖
ALTER TABLE products DROP COLUMN deleted_at;
ពេលណាត្រូវ denormalize។ Normalization ដោះដូរទំហំផ្ទុក និងភាពសាមញ្ញនៃការសរសេរ ដើម្បីភាពត្រឹមត្រូវ។ ម្តងម្កាលអ្នកបំពានច្បាប់ដោយចេតនា ជាទូទៅដើម្បីល្បឿន ឬប្រវត្តិ៖
- Snapshot ដូចជា
unit_priceដែលបានរៀបរាប់ខាងលើ។ - ផលបូកដែលរក្សាទុកជាមុន (cached total) ដូចជា column
orders.totalដើម្បីកុំឱ្យទំព័របញ្ជីបូករាប់ពាន់ row។ អ្នកត្រូវរក្សាវាឱ្យស្របគ្នា ល្អបំផុតក្នុង transaction តែមួយ (ជំពូកទី 12)។ - Reporting table ដែលចម្លងរាល់យប់ពី table ធម្មតាសម្រាប់ការវិភាគ។
លំដាប់មានសារៈសំខាន់៖ normalize មុន វាស់ ហើយបន្ទាប់មក denormalize ចំណុចជាក់លាក់មួយដោយមានហេតុផលដែលអ្នកអាចសរសេរចុះបាន។ កុំចាប់ផ្តើមជា flat "ដើម្បីដំណើរការ" មុនពេលអ្នកមានបញ្ហាដំណើរការ។ ជំពូកទី 11 បង្ហាញពីរបៀបដែល index ដោះស្រាយភាគច្រើនដោយមិនបំបែកការរចនា។
12. បញ្ជីត្រួតពិនិត្យការរចនា
មុនពេលអ្នកប្តេជ្ញា table ថ្មី សូមដើរកាត់បញ្ជីនេះ៖
- តើ row ជាអ្វីមួយ? និយាយវាជាប្រយោគមួយ៖ "row មួយគឺអតិថិជនម្នាក់"។ ប្រសិនបើអ្នកត្រូវការ "និង" អ្នកមាន table ពីរ។
- តើវាមាន primary key ឬទេ? ជាធម្មតាជា surrogate
id។ - តើគ្រប់ column ជាការពិតអំពីវត្ថុតែមួយនោះឬទេ? ប្រសិនបើមិនមែន ផ្លាស់ទីវា។
- តើ column ណាមួយធ្វើឡើងវិញនូវបញ្ជីឬទេ?
a, b, cក្នុង cell៖ បង្កើត child table។ - តើមានទំនាក់ទំនងអ្វីខ្លះ? សម្រាប់នីមួយៗ សម្រេច one-to-many ឬ many-to-many និងកន្លែងដែល foreign key ស្ថិតនៅ (តែងតែនៅខាង "many")។
- តើមានអ្វីកើតឡើងពេលលុប? ជ្រើស
RESTRICT,CASCADEឬSET NULLដោយចេតនា មិនមែនតាមលំនាំដើមទេ។ - តើអ្វីដែលមិនត្រូវតែជា NULL អវិជ្ជមាន ឬស្ទួន? សរសេរ
NOT NULL,CHECKនិងUNIQUE។ - Type ត្រឹមត្រូវឬទេ?
numericសម្រាប់ប្រាក់timestamptzសម្រាប់ពេលវេលា។ - តើច្បាប់ចម្លងណាជា snapshot? ចងក្រងឯកសារវា ដូចជាជាមួយ
unit_price។ - តើ column ណាដែល query នឹង filter ឬ JOIN? អ្នកនឹងដាក់ index លើវាក្នុងជំពូកបន្ទាប់ រួមទាំង foreign key។
13. Migration៖ ឯកសារ SQL ដែលមានលេខ
ការរចនាដំបូងរបស់អ្នកមិនដែលជាការរចនាចុងក្រោយទេ។ តម្រូវការផ្លាស់ប្តូរ ហើយមូលដ្ឋានទិន្នន័យមានទិន្នន័យពិតរួចហើយ។ Migration គឺជាឯកសារ SQL តូចមួយដែលផ្លាស់ប្តូរ schema ពីកំណែមួយទៅកំណែបន្ទាប់។ អ្នកបានសរសេររួចហើយមួយ៖ នៅជំពូកទី 6 អ្នកបានកែ products ឱ្យប្រើ category_id។
ទម្លាប់សាមញ្ញដែលមិនត្រូវការ tool គឺរក្សា migration ជា ឯកសារដែលមានលេខ ដែលដំណើរការតាមលំដាប់៖
shopdb/
schema.sql # schema ពេញលេញបច្ចុប្បន្ន សម្រាប់ការតំឡើងថ្មី
seed.sql
migrations/
001_add_categories.sql
002_add_orders_and_items.sql
003_add_deleted_at_to_products.sql
migration មានតែការផ្លាស់ប្តូរប៉ុណ្ណោះ ហើយល្អបំផុតគឺមានសុវត្ថិភាពក្នុងការអាន៖
-- migrations/003_add_deleted_at_to_products.sql
BEGIN;
ALTER TABLE products ADD COLUMN deleted_at timestamptz;
COMMIT;
ច្បាប់ដែលរក្សាវាឱ្យសមហេតុផល៖
- កុំកែ migration ដែលបានដំណើរការរួចហើយ លើមូលដ្ឋានទិន្នន័យចែករំលែក។ បន្ថែមមួយថ្មីដែលមានលេខខ្ពស់ជាងជំនួសវិញ។
- គោលបំណងមួយក្នុងមួយឯកសារ ដើម្បីឱ្យការបរាជ័យងាយយល់។
- អនុវត្តតាមលំដាប់ ហើយកត់ត្រាថាមួយណាបានដំណើរការ (framework រក្សា table សម្រាប់រឿងនេះ ដោយដៃ ចំណាំក្នុង README ដំណើរការសម្រាប់គម្រោងសិក្សា)។
docker exec -i shopdb-pg psql -U postgres -d shopdb \
< migrations/003_add_deleted_at_to_products.sql
គម្រោងពិតប្រើ migration tool (Flyway, Prisma Migrate, TypeORM និងផ្សេងទៀត) ដែលតាមដានរឿងនេះឱ្យអ្នក។ គំនិតគឺដូចគ្នាទៅនឹងឯកសារដែលមានលេខដែលអ្នកទើបតែបង្កើត។
14. សាកល្បងដោយខ្លួនឯង
- រកឱ្យឃើញ anomaly។ table
employees(id, name, department_name, department_phone)រក្សាទុកលេខទូរស័ព្ទនាយកដ្ឋានលើនិយោជិតគ្រប់រូប។ ដាក់ឈ្មោះ update anomaly មួយ insert anomaly មួយ និង delete anomaly មួយ។ តើវាបំពាន normal form ណា? - ជួសជុល column បញ្ជី។ table មួយមាន
students(id, name, courses)ដែលcoursesផ្ទុក'Math, SQL, Art'។ រចនា table ជំនួសជាមួយ key។ តើវាជាប្រភេទទំនាក់ទំនងអ្វី? - Normalize វា។ យក flat table នេះ ហើយរចនា table (column និង key) ដែលវាគួរក្លាយជា៖
invoices(invoice_no, date, client_name, client_address, item_description, item_price, qty)។ តើ column ណាដែលអ្នករក្សាជា snapshot ហើយហេតុអ្វី? - ដំណើរការការពិសោធតម្លៃ។ ធ្វើការពិសោធ
UPDATE products SET priceពីផ្នែកទី 8 លើមូលដ្ឋានទិន្នន័យរបស់អ្នកផ្ទាល់។ បន្ទាប់មកសរសេរ query ដែលបង្ហាញសម្រាប់គ្រប់ order item នូវភាពខុសគ្នារវាងតម្លៃឥឡូវនេះ និងតម្លៃដែលបានបង់។ - បន្ថែមប្រព័ន្ធ tag។ បង្កើត
tagsនិងproduct_tagsពីផ្នែកទី 9 ជាឯកសារ migration005_add_tags.sql។ បញ្ចូល tag ពីរ ហើយភ្ជាប់ពួកវាទៅផលិតផលបី បន្ទាប់មកបង្ហាញបញ្ជីផលិតផលនីមួយៗជាមួយ tag របស់វាដោយប្រើ JOIN។ - ត្រួតពិនិត្យ table របស់អ្នកផ្ទាល់។ ជ្រើស table មួយពីគម្រោងដែលអ្នកបានសាងសង់ (ឬពីហាង)។ ដើរកាត់បញ្ជីត្រួតពិនិត្យក្នុងផ្នែកទី 12។ សរសេរចុះរឿងមួយដែលអ្នកនឹងផ្លាស់ប្តូរ។
ទៅទីណាបន្ត។ ការរចនារបស់អ្នកស្អាត និងត្រឹមត្រូវ។ សំណួរបន្ទាប់គឺល្បឿន៖ តើមានអ្វីកើតឡើងពេល orders មានមួយលាន row ជំនួសឱ្យប្រាំ? ជំពូកទី 11, Indexes និង Query Performance បង្ហាញពីរបៀបដែល PostgreSQL រក row របៀបអាន EXPLAIN និងរបៀបដែល CREATE INDEX មួយអាចបំប្លែង query យឺតទៅជា query ភ្លាមៗ។
ទិញកាហ្វេជូនខ្ញុំ
ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!