Apakah ada opsi / fitur MySQL untuk melacak riwayat perubahan pada catatan?

122

Saya telah ditanya apakah saya dapat melacak perubahan pada catatan dalam database MySQL. Jadi ketika sebuah bidang telah diubah, yang lama vs yang baru tersedia dan tanggal ini terjadi. Apakah ada ciri atau teknik umum untuk melakukan ini?

Jika demikian, saya berpikir untuk melakukan sesuatu seperti ini. Buat tabel bernama changes. Ini akan berisi bidang yang sama dengan tabel master tetapi diawali dengan yang lama dan yang baru, tetapi hanya untuk bidang yang benar-benar diubah dan TIMESTAMPuntuk itu. Ini akan diindeks dengan ID. Dengan cara ini, sebuah SELECTlaporan dapat dijalankan untuk menunjukkan sejarah setiap record. Apakah ini metode yang bagus? Terima kasih!

Edward
sumber

Jawaban:

83

Itu halus.

Jika persyaratan bisnis adalah "Saya ingin mengaudit perubahan data - siapa yang melakukan apa dan kapan?", Anda biasanya dapat menggunakan tabel audit (sesuai contoh pemicu yang diposting Keethanjan). Saya bukan penggemar berat pemicu, tetapi memiliki manfaat besar karena relatif tidak menyakitkan untuk diterapkan - kode Anda yang ada tidak perlu tahu tentang pemicu dan audit.

Jika persyaratan bisnisnya adalah "tunjukkan keadaan data pada tanggal tertentu di masa lalu", itu berarti aspek perubahan dari waktu ke waktu telah memasuki solusi Anda. Meskipun Anda dapat, hampir, merekonstruksi status database hanya dengan melihat tabel audit, itu sulit dan rawan kesalahan, dan untuk logika database yang rumit, itu menjadi sulit. Misalnya, jika bisnis ingin mengetahui "menemukan alamat surat yang seharusnya kami kirimkan kepada pelanggan yang memiliki faktur terutang dan belum dibayar pada hari pertama bulan itu", Anda mungkin harus menjaring setengah lusin tabel audit.

Sebaliknya, Anda dapat memasukkan konsep perubahan dari waktu ke waktu ke dalam desain skema Anda (ini adalah opsi kedua yang disarankan Keethanjan). Ini adalah perubahan pada aplikasi Anda, pasti pada logika bisnis dan tingkat ketekunan, jadi tidak sepele.

Misalnya, jika Anda memiliki tabel seperti ini:

CUSTOMER
---------
CUSTOMER_ID PK
CUSTOMER_NAME
CUSTOMER_ADDRESS

dan Anda ingin terus memantaunya, Anda harus mengubahnya sebagai berikut:

CUSTOMER
------------
CUSTOMER_ID            PK
CUSTOMER_VALID_FROM    PK
CUSTOMER_VALID_UNTIL   PK
CUSTOMER_STATUS
CUSTOMER_USER
CUSTOMER_NAME
CUSTOMER_ADDRESS

Setiap kali Anda ingin mengubah catatan pelanggan, alih-alih memperbarui catatan, Anda menetapkan VALID_UNTIL pada catatan saat ini ke SEKARANG (), dan memasukkan catatan baru dengan VALID_FROM (sekarang) dan null VALID_UNTIL. Anda menyetel status "CUSTOMER_USER" ke ID login pengguna saat ini (jika Anda perlu menyimpannya). Jika pelanggan perlu dihapus, Anda menggunakan bendera CUSTOMER_STATUS untuk menunjukkan ini - Anda tidak boleh menghapus catatan dari tabel ini.

Dengan begitu, Anda selalu dapat menemukan status tabel pelanggan untuk tanggal tertentu - apa alamatnya? Apakah mereka sudah mengganti nama? Dengan bergabung ke tabel lain dengan tanggal valid_from dan valid_until yang serupa, Anda dapat merekonstruksi keseluruhan gambar secara historis. Untuk menemukan status saat ini, Anda mencari record dengan tanggal null VALID_UNTIL.

Ini sulit (sebenarnya, Anda tidak memerlukan valid_from, tetapi ini membuat kueri sedikit lebih mudah). Ini memperumit desain dan akses database Anda. Tapi itu membuat rekonstruksi dunia jauh lebih mudah.

Neville Kuyt
sumber
Tapi itu akan menambah data duplikat untuk bidang-bidang yang tidak diperbarui? Bagaimana cara mengelolanya?
itzmukeshy7
Dengan pendekatan kedua masalah muncul untuk pembuatan laporan jika catatan pelanggan diedit dari waktu ke waktu sulit untuk mengenali apakah entri tertentu milik pelanggan yang sama atau berbeda.
Akshay Joshi
Saran terbaik yang pernah saya lihat untuk masalah ini
Layak7
Oh dan, sebagai tanggapan atas komentar, bagaimana jika hanya menyimpan null untuk semua hal lain yang tidak berubah? Jadi versi terbaru adalah semua data terbaru, tetapi jika nama yang digunakan adalah "Bob" 5 hari yang lalu, maka hanya memiliki satu baris, name = bob dan berlaku sampai 5 hari yang lalu.
Layak7
2
Kombinasi customer_id dan tanggal adalah kunci utama, sehingga dijamin unik.
Neville Kuyt
186

Berikut cara langsung untuk melakukan ini:

Pertama, buat tabel riwayat untuk setiap tabel data yang ingin Anda lacak (contoh kueri di bawah). Tabel ini akan memiliki entri untuk setiap kueri penyisipan, pembaruan, dan penghapusan yang dilakukan pada setiap baris dalam tabel data.

Struktur tabel sejarah akan sama dengan tabel data yang dilacak kecuali tiga kolom tambahan: kolom untuk menyimpan operasi yang terjadi (sebut saja 'tindakan'), tanggal dan waktu operasi, dan kolom untuk menyimpan nomor urut ('revisi'), yang bertambah per operasi dan dikelompokkan berdasarkan kolom kunci utama dari tabel data.

Untuk melakukan perilaku pengurutan ini, indeks dua kolom (komposit) dibuat pada kolom kunci utama dan kolom revisi. Perhatikan bahwa Anda hanya dapat melakukan pengurutan dengan cara ini jika mesin yang digunakan oleh tabel riwayat adalah MyISAM ( Lihat 'Catatan MyISAM' di halaman ini)

Tabel riwayat cukup mudah dibuat. Di kueri ALTER TABLE di bawah (dan di kueri pemicu di bawahnya), ganti 'primary_key_column' dengan nama sebenarnya dari kolom itu di tabel data Anda.

CREATE TABLE MyDB.data_history LIKE MyDB.data;

ALTER TABLE MyDB.data_history MODIFY COLUMN primary_key_column int(11) NOT NULL, 
   DROP PRIMARY KEY, ENGINE = MyISAM, ADD action VARCHAR(8) DEFAULT 'insert' FIRST, 
   ADD revision INT(6) NOT NULL AUTO_INCREMENT AFTER action,
   ADD dt_datetime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER revision,
   ADD PRIMARY KEY (primary_key_column, revision);

Dan kemudian Anda membuat pemicu:

DROP TRIGGER IF EXISTS MyDB.data__ai;
DROP TRIGGER IF EXISTS MyDB.data__au;
DROP TRIGGER IF EXISTS MyDB.data__bd;

CREATE TRIGGER MyDB.data__ai AFTER INSERT ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'insert', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__au AFTER UPDATE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'update', NULL, NOW(), d.*
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__bd BEFORE DELETE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'delete', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = OLD.primary_key_column;

Dan Anda sudah selesai. Sekarang, semua penyisipan, pembaruan, dan penghapusan di 'MyDb.data' akan disimpan di 'MyDb.data_history', memberi Anda tabel riwayat seperti ini (minus kolom 'data_columns' yang dibuat-buat)

ID    revision   action    data columns..
1     1         'insert'   ....          initial entry for row where ID = 1
1     2         'update'   ....          changes made to row where ID = 1
2     1         'insert'   ....          initial entry, ID = 2
3     1         'insert'   ....          initial entry, ID = 3 
1     3         'update'   ....          more changes made to row where ID = 1
3     2         'update'   ....          changes made to row where ID = 3
2     2         'delete'   ....          deletion of row where ID = 2 

Untuk menampilkan perubahan pada kolom atau kolom tertentu dari pembaruan ke pembaruan, Anda harus menggabungkan tabel riwayat ke tabel itu sendiri pada kunci utama dan kolom urutan. Anda dapat membuat tampilan untuk tujuan ini, misalnya:

CREATE VIEW data_history_changes AS 
   SELECT t2.dt_datetime, t2.action, t1.primary_key_column as 'row id', 
   IF(t1.a_column = t2.a_column, t1.a_column, CONCAT(t1.a_column, " to ", t2.a_column)) as a_column
   FROM MyDB.data_history as t1 INNER join MyDB.data_history as t2 on t1.primary_key_column = t2.primary_key_column 
   WHERE (t1.revision = 1 AND t2.revision = 1) OR t2.revision = t1.revision+1
   ORDER BY t1.primary_key_column ASC, t2.revision ASC

Sunting: Oh wow, orang-orang menyukai tabel sejarah saya dari 6 tahun yang lalu: P

Implementasi saya tentang hal itu masih terus berlanjut, menjadi lebih besar dan lebih berat, saya kira. Saya menulis tampilan dan UI yang cukup bagus untuk melihat sejarah dalam database ini, tapi saya rasa itu tidak pernah banyak digunakan. Begitu seterusnya.

Untuk menjawab beberapa komentar tanpa urutan tertentu:

  • Saya melakukan implementasi saya sendiri di PHP yang sedikit lebih terlibat, dan menghindari beberapa masalah yang dijelaskan dalam komentar (memiliki indeks ditransfer, secara signifikan. Jika Anda mentransfer indeks unik ke tabel riwayat, semuanya akan rusak. Ada solusi untuk ini di komentar). Mengikuti posting ini ke surat itu bisa menjadi petualangan, tergantung pada seberapa mapan database Anda.

  • Jika hubungan antara kunci utama dan kolom revisi tampak tidak aktif, biasanya kunci komposit mengalami gangguan. Pada beberapa kesempatan langka, saya mengalami hal ini dan kehilangan tujuan.

  • Saya menemukan solusi ini cukup berkinerja, menggunakan pemicu seperti itu. Juga, MyISAM cepat dalam menyisipkan, yang dilakukan oleh semua pemicu. Anda dapat meningkatkan ini lebih jauh dengan pengindeksan cerdas (atau kurangnya ...). Memasukkan satu baris ke dalam tabel MyISAM dengan kunci utama seharusnya tidak menjadi operasi yang perlu Anda optimalkan, sungguh, kecuali Anda memiliki masalah signifikan yang terjadi di tempat lain. Selama saya menjalankan database MySQL, implementasi tabel riwayat ini, tidak pernah menjadi penyebab dari (banyak) masalah performa yang muncul.

  • jika Anda mendapatkan penyisipan berulang, periksa lapisan perangkat lunak Anda untuk INSERT IGNORE jenis kueri. Hrmm, tidak dapat mengingatnya sekarang, tetapi saya pikir ada masalah dengan skema dan transaksi ini yang akhirnya gagal setelah menjalankan beberapa tindakan DML. Sesuatu yang harus diperhatikan, setidaknya.

  • Bidang di tabel riwayat dan tabel data harus cocok. Atau, tabel data Anda tidak memiliki lebih banyak kolom daripada tabel riwayat. Jika tidak, kueri sisipkan / perbarui / del pada tabel data akan gagal, saat penyisipan ke tabel riwayat meletakkan kolom dalam kueri yang tidak ada (karena d. * Dalam kueri pemicu), dan pemicu gagal. Ini akan menjadi luar biasa jika MySQL memiliki sesuatu seperti pemicu skema, di mana Anda dapat mengubah tabel riwayat jika kolom ditambahkan ke tabel data. Apakah MySQL memilikinya sekarang? Saya melakukan React hari ini: P

penutupan sementara
sumber
3
saya sangat suka solusi ini. Namun jika tabel utama Anda tidak memiliki kunci utama atau Anda tidak tahu apa itu kunci utama, itu agak rumit.
Benjamin Eckstein
1
Saya baru-baru ini mengalami masalah saat menggunakan solusi ini untuk sebuah proyek, karena bagaimana semua indeks dari tabel asli disalin ke tabel riwayat (karena cara kerja CREATE TABLE ... LIKE ....). Memiliki indeks unik di tabel riwayat dapat menyebabkan kueri INSERT di pemicu SETELAH UPDATE menjadi muntah, jadi mereka perlu dihapus. Dalam skrip php saya yang melakukan hal ini, saya meminta indeks unik apa pun pada tabel riwayat yang baru dibuat (dengan "TAMPILKAN INDEKS DARI data_table WHERE Key_name! = 'PRIMARY' dan Non_unique = 0"), lalu hapus.
penutupan sementara
3
Di sini kita mendapatkan data berulang yang dimasukkan ke tabel cadangan setiap saat. Misalkan jika kita memiliki 10 field dalam sebuah tabel dan kita telah mengupdate 2 maka kita menambahkan data berulang untuk 8 field lainnya. Bagaimana cara mengatasinya?
itzmukeshy7
6
Anda dapat menghindari terbawa berbagai indeks secara tidak sengaja dengan mengubah pernyataan buat tabel menjadiCREATE TABLE MyDB.data_history as select * from MyDB.data limit 0;
Eric Hayes
4
@transientclosure bagaimana Anda mengusulkan memasukkan bidang lain ke dalam riwayat yang bukan bagian dari kueri asli? misalnya saya ingin melacak siapa yang membuat perubahan itu. untuk memasukkan itu sudah memiliki ownerbidang, dan untuk pembaruan saya dapat menambahkan updatedbybidang, tetapi untuk menghapus saya tidak yakin bagaimana saya bisa melakukannya melalui pemicu. memperbarui data_historybaris dengan id pengguna di terasa kotor: P
Kuda
16

Anda bisa membuat pemicu untuk menyelesaikan ini. Berikut adalah tutorial untuk melakukannya (tautan yang diarsipkan).

Menetapkan batasan dan aturan dalam database lebih baik daripada menulis kode khusus untuk menangani tugas yang sama karena hal itu akan mencegah pengembang lain menulis kueri berbeda yang mengabaikan semua kode khusus dan dapat membuat database Anda dengan integritas data yang buruk.

Untuk waktu yang lama saya menyalin info ke tabel lain menggunakan skrip karena MySQL tidak mendukung pemicu pada saat itu. Sekarang saya telah menemukan pemicu ini lebih efektif dalam melacak segalanya.

Pemicu ini akan menyalin nilai lama ke tabel riwayat jika diubah ketika seseorang mengedit baris. Editor IDdan last moddisimpan di tabel asli setiap kali seseorang mengedit baris itu; waktu sesuai dengan saat diubah ke bentuknya saat ini.

DROP TRIGGER IF EXISTS history_trigger $$

CREATE TRIGGER history_trigger
BEFORE UPDATE ON clients
    FOR EACH ROW
    BEGIN
        IF OLD.first_name != NEW.first_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'first_name',
                        NEW.first_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

        IF OLD.last_name != NEW.last_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'last_name',
                        NEW.last_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

    END;
$$

Solusi lain adalah menyimpan bidang Revisi dan memperbarui bidang ini saat disimpan. Anda dapat memutuskan bahwa maks adalah revisi terbaru, atau 0 adalah baris terbaru. Terserah kamu.

Keethanjan
sumber
9

Inilah cara kami menyelesaikannya

tabel Pengguna terlihat seperti ini

Users
-------------------------------------------------
id | name | address | phone | email | created_on | updated_on

Dan persyaratan bisnis berubah dan kami harus memeriksa semua alamat dan nomor telepon sebelumnya yang pernah dimiliki pengguna. skema baru terlihat seperti ini

Users (the data that won't change over time)
-------------
id | name

UserData (the data that can change over time and needs to be tracked)
-------------------------------------------------
id | id_user | revision | city | address | phone | email | created_on
 1 |   1     |    0     | NY   | lake st | 9809  | @long | 2015-10-24 10:24:20
 2 |   1     |    2     | Tokyo| lake st | 9809  | @long | 2015-10-24 10:24:20
 3 |   1     |    3     | Sdny | lake st | 9809  | @long | 2015-10-24 10:24:20
 4 |   2     |    0     | Ankr | lake st | 9809  | @long | 2015-10-24 10:24:20
 5 |   2     |    1     | Lond | lake st | 9809  | @long | 2015-10-24 10:24:20

Untuk menemukan alamat saat ini dari setiap pengguna, kami mencari UserData dengan revisi DESC dan LIMIT 1

Untuk mendapatkan alamat pengguna antara periode waktu tertentu kita dapat menggunakan create_on bewteen (tanggal1, tanggal 2)

Zenex
sumber
Ini adalah solusi yang ingin saya miliki tetapi saya ingin tahu Bagaimana Anda dapat memasukkan id_user dalam tabel ini menggunakan pemicu?
thecassion
1
Apa yang terjadi dengan revision=1para id_user=1? Pertama saya mengira bahwa penghitungan Anda adalah 0,2,3,...tetapi kemudian saya melihat bahwa untuk id_user=2penghitungan revisi adalah0,1, ...
Pathros
1
Anda tidak perlu iddan id_userkolom . Just use a group ID of id` (ID pengguna) dan revision.
Gajus
6

MariaDB mendukung Pembuatan Versi Sistem sejak 10.3 yang merupakan fitur SQL standar yang melakukan apa yang Anda inginkan: ia menyimpan riwayat catatan tabel dan menyediakan akses ke sana melalui SELECTkueri. MariaDB adalah fork pengembangan terbuka dari MySQL. Anda dapat menemukan lebih lanjut tentang Pembuatan Versi Sistem melalui tautan ini:

https://mariadb.com/kb/en/library/system-versioned-tables/

midenok
sumber
Harap perhatikan hal berikut dari tautan di atas: "mysqldump tidak membaca baris historis dari tabel berversi, sehingga data historis tidak akan dicadangkan. Selain itu, pemulihan stempel waktu tidak dapat dilakukan karena tidak dapat ditentukan dengan sisipan / pengguna."
Daniel
4

Mengapa tidak menggunakan file log bin? Jika replikasi diatur di server Mysql, dan format file binlog diatur ke ROW, maka semua perubahan dapat ditangkap.

Pustaka python yang bagus bernama noplay dapat digunakan. Info selengkapnya di sini .

Ouroboros
sumber
2
Binlog dapat digunakan meskipun Anda tidak memiliki / membutuhkan replikasi. Binlog memiliki banyak kasus penggunaan yang bermanfaat. Replikasi mungkin merupakan kasus penggunaan yang paling umum, tetapi juga dapat dimanfaatkan untuk cadangan dan riwayat audit, seperti yang disebutkan di sini.
webaholik
3

Hanya 2 sen saya. Saya akan membuat solusi yang mencatat dengan tepat apa yang berubah, sangat mirip dengan solusi transien.

My ChangesTable sederhana menjadi:

DateTime | WhoChanged | TableName | Action | ID |FieldName | OldValue

1) Ketika seluruh baris diubah di tabel utama, banyak entri akan masuk ke tabel ini, TAPI itu sangat tidak mungkin, jadi bukan masalah besar (orang biasanya hanya mengubah satu hal) 2) OldVaue (dan NewValue jika Anda ingin) harus menjadi semacam "jenis apa saja" epik karena bisa berupa data apa pun, mungkin ada cara untuk melakukan ini dengan jenis RAW atau hanya menggunakan string JSON untuk mengonversi masuk dan keluar.

Penggunaan data minimum, menyimpan semua yang Anda butuhkan dan dapat digunakan untuk semua tabel sekaligus. Saya sedang meneliti ini sendiri sekarang, tetapi ini mungkin akhirnya menjadi cara saya pergi.

Untuk Buat dan Hapus, cukup ID baris, tidak perlu bidang. Jika menghapus sebuah bendera di meja utama (aktif?) Akan bagus.

Layak7
sumber
0

Cara langsung untuk melakukan ini adalah dengan membuat pemicu di atas tabel. Tetapkan beberapa kondisi atau metode pemetaan. Ketika pembaruan atau penghapusan terjadi, itu akan dimasukkan ke dalam tabel 'ubah' secara otomatis.

Tetapi bagian terbesar adalah bagaimana jika kita mendapat banyak kolom dan banyak tabel. Kami harus mengetikkan setiap nama kolom dari setiap tabel. Jelas sekali, Ini hanya membuang-buang waktu.

Untuk menangani ini dengan lebih baik, kita dapat membuat beberapa prosedur atau fungsi untuk mengambil nama kolom.

Kami juga dapat menggunakan alat bagian ke-3 hanya untuk melakukan ini. Di sini, saya menulis program java Mysql Tracker

goforu
sumber
bagaimana cara menggunakan Pelacak Mysql Anda?
webchun
1
1. Pastikan Anda memiliki kolom id sebagai kunci utama di setiap tabel. 2. Salin file java ke local (atau IDE) 3. Impor libs dan edit variabel statis dari baris 9-15 sesuai dengan konfigurasi dan struktur database Anda. 4. Parse dan jalankan file java 5. Salin log konsol dan jalankan sebagai perintah Mysql
goforu
create table like tablesaya pikir mereplikasi semua kolom dengan mudah
Jonathan