Normalization និងការរចនា Schema

មាតិកា

  1. អន្ទាក់ Table មួយធំ
  2. Anomaly ទាំងបី
  3. គំនិតនៅពីក្រោយ Normalization
  4. Normal Form ទី 1៖ មួយតម្លៃក្នុងមួយ Cell
  5. Normal Form ទី 2៖ ពឹងលើ Key ទាំងមូល
  6. Normal Form ទី 3៖ គ្មានអ្វីក្រៅពី Key
  7. ពិនិត្យ shopdb
  8. ហេតុអ្វី unit_price ជាច្បាប់ចម្លងដោយចេតនា
  9. Many-to-Many និងអនុសញ្ញាដាក់ឈ្មោះ
  10. ការជ្រើសរើស Data Type ឱ្យបានល្អ
  11. Soft Delete និងពេលណាត្រូវ Denormalize
  12. បញ្ជីត្រួតពិនិត្យការរចនា
  13. Migration៖ ឯកសារ SQL ដែលមានលេខ
  14. សាកល្បងដោយខ្លួនឯង

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_nameorder (តាមពិតគឺអតិថិជន)customers / orders
product_priceផលិតផលproducts
quantityorder និងផលិតផល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, orderstable គឺជាបណ្តុំ
ឈ្មោះ column ជាឯកវចនៈname, statuscolumn គឺជាការពិតមួយ
snake_case អក្សរតូចcreated_atជៀសវាងបញ្ហា quoting ក្នុង PostgreSQL
Primary key ជា idcustomers.idឈ្មោះដូចគ្នាជានិច្ច
Foreign key ជា <table>_idcustomer_idអានថា "ចង្អុលទៅអតិថិជន"
Timestamp បញ្ចប់ដោយ _atordered_atប្រភេទច្បាស់
Boolean អានជាសំណួរis_activeWHERE is_active អានដូចភាសាអង់គ្លេស

PostgreSQL បំប្លែងឈ្មោះដែលគ្មាន quote ទៅជាអក្សរតូច ដូច្នេះ CustomerName ក្លាយជា customername។ អក្សរធំលាយអក្សរតូចបង្ខំអ្នកឱ្យ quote ជារៀងរហូត។ ប្រកាន់ snake_case ហើយមិនបាច់គិតអំពីវាទៀត។

ជ្រើសរើសអនុសញ្ញាមួយ ហើយរក្សាវា។ ភាពស្របគ្នាសំខាន់ជាងការដែលអ្នកជ្រើសមួយណា។

10. ការជ្រើសរើស Data Type ឱ្យបានល្អ

type របស់ column ក៏ជា constraint មួយដែរ។ សូមពិនិត្យឡើងវិញនូវជម្រើសពីជំពូកទី 3 ដោយគិតអំពីការរចនា៖

ទិន្នន័យប្រើជៀសវាងហេតុផល
ប្រាក់numeric(10,2)real, double precisionFloat មិនអាចរក្សាទុក 0.10 បានត្រឹមត្រូវទេ
Timestamptimestamptztimestamp, textរក្សាទុកពេលវេលាច្បាស់លាស់ ហើយបំប្លែងតាមល្វែងម៉ោងនៃ session
កាលបរិច្ឆេទតែប៉ុណ្ណោះdatetext '2026-01-05'មានការគណនាកាលបរិច្ឆេទ និង validation ឥតគិតថ្លៃ
បាទ/ចាស ឬទេbooleaninteger 0/1, 'Y'/'N'ច្បាស់លាស់ និងមានការត្រួតពិនិត្យ
ជម្រើសថេរtext + CHECK ... IN (...)free textកំហុសអក្ខរាវិរុទ្ធដូចជា 'shiped' ត្រូវបានបដិសេធ
ឈ្មោះ អ៊ីមែលtextvarchar(50) ដែលស្មានPostgres គ្មានទណ្ឌកម្មល្បឿន ហើយអ្នកជៀសវាង bug ការកាត់ខ្លី (truncation)
Surrogate keyinteger GENERATED ALWAYS AS IDENTITYserial សម្រាប់កូដថ្មីSQL ស្តង់ដារ ពិបាកសរសេរជាន់ដោយចៃដន្យ
ID សាធារណៈuuidid តាមលំដាប់ក្នុង 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 ថ្មី សូមដើរកាត់បញ្ជីនេះ៖

  1. តើ row ជាអ្វីមួយ? និយាយវាជាប្រយោគមួយ៖ "row មួយគឺអតិថិជនម្នាក់"។ ប្រសិនបើអ្នកត្រូវការ "និង" អ្នកមាន table ពីរ។
  2. តើវាមាន primary key ឬទេ? ជាធម្មតាជា surrogate id។
  3. តើគ្រប់ column ជាការពិតអំពីវត្ថុតែមួយនោះឬទេ? ប្រសិនបើមិនមែន ផ្លាស់ទីវា។
  4. តើ column ណាមួយធ្វើឡើងវិញនូវបញ្ជីឬទេ? a, b, c ក្នុង cell៖ បង្កើត child table។
  5. តើមានទំនាក់ទំនងអ្វីខ្លះ? សម្រាប់នីមួយៗ សម្រេច one-to-many ឬ many-to-many និងកន្លែងដែល foreign key ស្ថិតនៅ (តែងតែនៅខាង "many")។
  6. តើមានអ្វីកើតឡើងពេលលុប? ជ្រើស RESTRICT, CASCADE ឬ SET NULL ដោយចេតនា មិនមែនតាមលំនាំដើមទេ។
  7. តើអ្វីដែលមិនត្រូវតែជា NULL អវិជ្ជមាន ឬស្ទួន? សរសេរ NOT NULL, CHECK និង UNIQUE។
  8. Type ត្រឹមត្រូវឬទេ? numeric សម្រាប់ប្រាក់ timestamptz សម្រាប់ពេលវេលា។
  9. តើច្បាប់ចម្លងណាជា snapshot? ចងក្រងឯកសារវា ដូចជាជាមួយ unit_price។
  10. តើ 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. សាកល្បងដោយខ្លួនឯង

  1. រកឱ្យឃើញ anomaly។ table employees(id, name, department_name, department_phone) រក្សាទុកលេខទូរស័ព្ទនាយកដ្ឋានលើនិយោជិតគ្រប់រូប។ ដាក់ឈ្មោះ update anomaly មួយ insert anomaly មួយ និង delete anomaly មួយ។ តើវាបំពាន normal form ណា?
  2. ជួសជុល column បញ្ជី។ table មួយមាន students(id, name, courses) ដែល courses ផ្ទុក 'Math, SQL, Art'។ រចនា table ជំនួសជាមួយ key។ តើវាជាប្រភេទទំនាក់ទំនងអ្វី?
  3. Normalize វា។ យក flat table នេះ ហើយរចនា table (column និង key) ដែលវាគួរក្លាយជា៖ invoices(invoice_no, date, client_name, client_address, item_description, item_price, qty)។ តើ column ណាដែលអ្នករក្សាជា snapshot ហើយហេតុអ្វី?
  4. ដំណើរការការពិសោធតម្លៃ។ ធ្វើការពិសោធ UPDATE products SET price ពីផ្នែកទី 8 លើមូលដ្ឋានទិន្នន័យរបស់អ្នកផ្ទាល់។ បន្ទាប់មកសរសេរ query ដែលបង្ហាញសម្រាប់គ្រប់ order item នូវភាពខុសគ្នារវាងតម្លៃឥឡូវនេះ និងតម្លៃដែលបានបង់។
  5. បន្ថែមប្រព័ន្ធ tag។ បង្កើត tags និង product_tags ពីផ្នែកទី 9 ជាឯកសារ migration 005_add_tags.sql។ បញ្ចូល tag ពីរ ហើយភ្ជាប់ពួកវាទៅផលិតផលបី បន្ទាប់មកបង្ហាញបញ្ជីផលិតផលនីមួយៗជាមួយ tag របស់វាដោយប្រើ JOIN។
  6. ត្រួតពិនិត្យ table របស់អ្នកផ្ទាល់។ ជ្រើស table មួយពីគម្រោងដែលអ្នកបានសាងសង់ (ឬពីហាង)។ ដើរកាត់បញ្ជីត្រួតពិនិត្យក្នុងផ្នែកទី 12។ សរសេរចុះរឿងមួយដែលអ្នកនឹងផ្លាស់ប្តូរ។

ទៅទីណាបន្ត។ ការរចនារបស់អ្នកស្អាត និងត្រឹមត្រូវ។ សំណួរបន្ទាប់គឺល្បឿន៖ តើមានអ្វីកើតឡើងពេល orders មានមួយលាន row ជំនួសឱ្យប្រាំ? ជំពូកទី 11, Indexes និង Query Performance បង្ហាញពីរបៀបដែល PostgreSQL រក row របៀបអាន EXPLAIN និងរបៀបដែល CREATE INDEX មួយអាចបំប្លែង query យឺតទៅជា query ភ្លាមៗ។

ទិញកាហ្វេជូនខ្ញុំ

ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!