Kayıtlarda yapılan değişikliklerin geçmişini izlemek için bir MySQL seçeneği / özelliği var mı?


122

Bir MySQL veritabanındaki kayıtlarda yapılan değişiklikleri takip edip edemeyeceğim soruldu. Dolayısıyla, bir alan değiştirildiğinde, eski ile yeninin karşılaştırması ve bunun gerçekleştiği tarih mevcuttur. Bunu yapmak için bir özellik veya ortak bir teknik var mı?

Öyleyse, böyle bir şey yapmayı düşünüyordum. Adlı bir tablo oluşturun changes. Ana tablo ile aynı alanları içerir, ancak eski ve yeninin önüne eklenir, ancak yalnızca gerçekten değiştirilmiş alanlar TIMESTAMPiçin ve bunun için a . Bir ID. Bu şekilde, SELECTher kaydın geçmişini göstermek için bir rapor çalıştırılabilir. Bu iyi bir yöntem mi? Teşekkürler!

Yanıtlar:


83

Bu ince.

İş gereksinimi "Verilerdeki değişiklikleri denetlemek istiyorum - kim neyi, ne zaman yaptı?" İse, genellikle denetim tablolarını kullanabilirsiniz (Keethanjan'ın gönderdiği tetikleme örneğine göre). Tetikleyicilerin büyük bir hayranı değilim, ancak uygulanması nispeten ağrısız olmanın büyük yararı var - mevcut kodunuzun tetikleyiciler ve denetim unsurlarını bilmesine gerek yok.

İş gereksinimi "bana geçmişte belirli bir tarihte verilerin durumunu göster" ise, bu, zaman içindeki değişim yönünün çözümünüze girdiği anlamına gelir. Sadece denetim tablolarına bakarak hemen hemen veritabanının durumunu yeniden yapılandırabilseniz de, bu zor ve hataya açıktır ve herhangi bir karmaşık veritabanı mantığı için kullanışsız hale gelir. Örneğin, işletme "ayın ilk günü ödenmemiş, ödenmemiş faturaları olan müşterilere göndermemiz gereken mektupların adreslerini bulmak" istiyorsa, muhtemelen yarım düzine denetim masasını taramanız gerekir.

Bunun yerine, zaman içinde değişim konseptini şema tasarımınızda pişirebilirsiniz (bu, Keethanjan'ın önerdiği ikinci seçenektir). Bu, uygulamanızda kesinlikle iş mantığı ve kalıcılık düzeyinde bir değişikliktir, bu yüzden önemsiz değildir.

Örneğin, böyle bir masanız varsa:

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

ve zaman içinde takip etmek istediyseniz, bunu aşağıdaki gibi değiştirirsiniz:

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

Bir müşteri kaydını her değiştirmek istediğinizde, kaydı güncellemek yerine geçerli kayıttaki VALID_UNTIL değerini NOW () olarak ayarlar ve VALID_FROM (şimdi) ve boş VALID_UNTIL ile yeni bir kayıt eklersiniz. "CUSTOMER_USER" durumunu mevcut kullanıcının oturum açma kimliği olarak ayarlarsınız (bunu tutmanız gerekiyorsa). Müşterinin silinmesi gerekiyorsa, bunu belirtmek için CUSTOMER_STATUS bayrağını kullanırsınız - bu tablodaki kayıtları asla silemezsiniz.

Bu şekilde, belirli bir tarih için müşteri tablosunun durumunu her zaman bulabilirsiniz - adres neydi? İsmini değiştirdiler mi? Benzer valid_from ve valid_until tarihlerine sahip diğer tablolara katılarak, tüm resmi geçmişte yeniden yapılandırabilirsiniz. Mevcut durumu bulmak için, boş VALID_UNTIL tarihli kayıtları ararsınız.

Hantaldır (kesinlikle konuşmak gerekirse, valid_from'a ihtiyacınız yoktur, ancak sorguları biraz daha kolaylaştırır). Tasarımınızı ve veritabanı erişiminizi karmaşıklaştırır. Ama dünyayı yeniden inşa etmeyi çok daha kolay hale getiriyor.


Ancak güncellenmemiş alanlar için yinelenen veriler ekler mi? Nasıl yönetilir?
itzmukeshy7

Rapor oluşturma için ikinci yaklaşım problemi, bir müşteri kaydı belirli bir süre içinde düzenlenirse, belirli bir girişin aynı müşteriye mi yoksa farklı bir müşteriye mi ait olduğunu anlamak zordur.
Akshay Joshi

Bu soruna şimdiye kadar gördüğüm en iyi öneri
Worthy7

Oh ve yorumlara yanıt olarak, değişmeyen her şey için boş değer depolamaya ne dersiniz? Yani en son sürüm, en son verilerin tümü olacaktır, ancak ad 5 gün önce "Bob" ise, o zaman sadece bir satıra sahip olun, name = bob ve 5 gün öncesine kadar geçerlidir.
Worthy7

2
Customer_id ve tarihlerin kombinasyonu birincil anahtardır, bu nedenle benzersiz olmaları garanti edilir.
Neville Kuyt

187

İşte bunu yapmanın basit bir yolu:

Öncelikle, izlemek istediğiniz her veri tablosu için bir geçmiş tablosu oluşturun (aşağıdaki örnek sorgu). Bu tablo, veri tablosundaki her satırda gerçekleştirilen her ekleme, güncelleme ve silme sorgusu için bir girişe sahip olacaktır.

Geçmiş tablosunun yapısı, üç ek sütun dışında izlediği veri tablosu ile aynı olacaktır: gerçekleşen işlemi saklamak için bir sütun (buna 'eylem' diyelim), işlemin tarihi ve saati ve bir sütun İşlem başına artan ve veri tablosunun birincil anahtar sütununa göre gruplanan bir sıra numarası ('revizyon') saklamak için.

Bu sıralama davranışını gerçekleştirmek için, birincil anahtar sütununda ve revizyon sütununda iki sütunlu (bileşik) bir dizin oluşturulur. Geçmiş tablosu tarafından kullanılan motor MyISAM ise yalnızca bu şekilde sıralama yapabileceğinizi unutmayın ( Bu sayfadaki 'MyISAM Notları'na bakın)

Geçmiş tablosunun oluşturulması oldukça kolaydır. Aşağıdaki ALTER TABLE sorgusunda (ve bunun altındaki tetikleme sorgularında), veri tablonuzdaki 'birincil_anahtar_sütun'u o sütunun gerçek adıyla değiştirin.

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

Ve sonra tetikleyicileri yaratırsınız:

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;

Ve bitirdiniz. Şimdi, 'MyDb.data'daki tüm ekler, güncellemeler ve silmeler,' MyDb.data_history'ye kaydedilecek ve size bunun gibi bir geçmiş tablosu verecek (çıkarılmış 'data_columns' sütunu)

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 

Güncellemeden güncellemeye kadar belirli bir sütun veya sütunlar için değişiklikleri görüntülemek için, geçmiş tablosunu birincil anahtar ve sıra sütunlarında kendisine eklemeniz gerekir. Bu amaçla bir görünüm oluşturabilirsiniz, örneğin:

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

Düzenleme: Vay canına, insanlar 6 yıl önceki tarih masamı seviyorlar: P

Bunu uygulamam hala mırıldanıyor, daha da büyüyor ve daha hantal hale geliyor. Bu veritabanındaki geçmişe bakmak için görüşler ve oldukça güzel bir kullanıcı arayüzü yazdım, ancak bunun çok fazla kullanıldığını sanmıyorum. O zaman o gider.

Bazı yorumları belirli bir sırayla ele almak için:

  • PHP'de biraz daha karmaşık olan kendi uygulamamı yaptım ve yorumlarda açıklanan bazı sorunlardan kaçındım (önemli ölçüde dizinlerin aktarılması. Benzersiz dizinler üzerinden geçmiş tablosuna aktarırsanız işler bozulacak. Çözümler var. bu yorumlarda). Bu yazıyı mektuba takip etmek, veritabanınızın ne kadar yerleşik olduğuna bağlı olarak bir macera olabilir.

  • Birincil anahtar ile revizyon sütunu arasındaki ilişki kapalı görünüyorsa, bu genellikle bileşik anahtarın bir şekilde çalıştırıldığı anlamına gelir. Birkaç ender olayda bu oldu ve nedenini kaybettim.

  • Tetikleyicileri olduğu gibi kullanarak bu çözümü oldukça performanslı buldum. Ayrıca MyISAM, tüm tetikleyicilerin yaptığı gibi eklerde hızlıdır. Bunu akıllı indeksleme (veya eksiklik ...) ile daha da iyileştirebilirsiniz. Bir MyISAM tablosuna birincil anahtarla tek bir satır eklemek, başka bir yerde önemli sorunlarınız olmadıkça, gerçekten optimize etmeniz gereken bir işlem olmamalıdır. MySQL veritabanını çalıştırdığım süre boyunca bu geçmiş tablosu uygulaması açıktı, hiçbir zaman ortaya çıkan (birçok) performans sorununun nedeni olmadı.

  • Tekrarlanan eklemeler alıyorsanız, INSERT IGNORE türü sorgular için yazılım katmanınızı kontrol edin. Hrmm, şimdi hatırlayamıyorum, ancak bu şema ve işlemlerde birden çok DML eylemi çalıştırdıktan sonra sonuçta başarısız olan sorunlar olduğunu düşünüyorum. En azından farkında olunması gereken bir şey.

  • Geçmiş tablosundaki ve veri tablosundaki alanların eşleşmesi önemlidir. Veya, veri tablonuzun geçmiş tablosundan DAHA FAZLA sütuna sahip olmamasıdır. Aksi takdirde, geçmiş tablolarına yapılan eklemeler sorguya var olmayan sütunlar koyduğunda (tetikleme sorgularındaki d. * Nedeniyle) ve tetikleyici başarısız olduğunda, veri tablosundaki ekleme / güncelleme / silme sorguları başarısız olur. MySQL, veri tablosuna sütunlar eklenmişse geçmiş tablosunu değiştirebileceğiniz şema tetikleyicileri gibi bir şeye sahip olsaydı harika olurdu. MySQL şimdi buna sahip mi? Bugünlerde React yapıyorum: P


3
Bu çözümü gerçekten beğendim. ancak ana tablonuzun birincil anahtarı yoksa veya birincil anahtarın ne olduğunu bilmiyorsanız, bu biraz zor olabilir.
Benjamin Eckstein

1
Yakın zamanda bir proje için bu çözümü kullanırken bir sorunla karşılaştım, çünkü orijinal tablodaki tüm dizinlerin geçmiş tablosuna nasıl kopyalanması nedeniyle (CREATE TABLE ... LIKE .... çalıştığı için). Geçmiş tablosunda benzersiz dizinlere sahip olmak, INSERT sorgusunun AFTER UPDATE tetikleyicisinde barf olmasına neden olabilir, bu nedenle kaldırılmaları gerekir. Php betiğinde bu işi yapan var, yeni oluşturulan geçmiş tablolarındaki benzersiz indeksleri sorguluyorum ("SHOW INDEX FROM data_table WHERE Key_name! = 'PRIMARY' ve Non_unique = 0" ile) ve sonra onları kaldırıyorum.
geçici kapanış

3
Burada her seferinde yedekleme tablosuna eklenen tekrarlanan verileri alıyoruz. Bir tabloda 10 alanımız varsa ve 2'yi güncellediysek, kalan 8 alan için tekrarlanan verileri ekliyoruz. Bundan nasıl kurtulunur?
itzmukeshy7

6
Create table deyiminiCREATE TABLE MyDB.data_history as select * from MyDB.data limit 0;
Eric Hayes

4
@transientclosure, geçmişe orijinal sorgunun bir parçası olmayan diğer alanları eklemeyi nasıl önerirsiniz? örneğin, bu değişiklikleri kimin yaptığını izlemek istiyorum. eklemek için zaten bir owneralan var ve güncelleme için bir updatedbyalan ekleyebilirim , ancak silmek için bunu tetikleyiciler aracılığıyla nasıl yapabileceğimi bilmiyorum. data_historysatırın kullanıcı kimliğiyle güncellenmesi kirli hissettiriyor: P
At

16

Bunu çözmek için tetikleyiciler oluşturabilirsiniz. İşte bunu yapmak için bir eğitim (arşivlenmiş bağlantı).

Veritabanında kısıtlamalar ve kurallar ayarlamak, aynı görevi yerine getirmek için özel kod yazmaktan daha iyidir, çünkü başka bir geliştiricinin tüm özel kodu atlayan farklı bir sorgu yazmasını engeller ve veritabanınızı zayıf veri bütünlüğüyle bırakabilir.

MySQL o sırada tetikleyicileri desteklemediğinden, uzun süredir bir komut dosyası kullanarak bilgileri başka bir tabloya kopyalıyordum. Şimdi bu tetikleyicinin her şeyi takip etmede daha etkili olduğunu buldum.

Bu tetikleyici, birisi bir satırı düzenlediğinde değiştirilirse eski bir değeri geçmiş tablosuna kopyalar. Editor IDve last modbiri bu satırı her düzenlediğinde orijinal tabloda saklanır; zaman, mevcut biçimine değiştirildiği zamana karşılık gelir.

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

Diğer bir çözüm, bir Revizyon alanı tutmak ve bu alanı kaydetme sırasında güncellemektir. Maksimumun en yeni revizyon olduğuna veya 0'ın en son satır olduğuna karar verebilirsiniz. Bu size kalmış.


9

İşte nasıl çözdük

Kullanıcılar tablosu şuna benziyordu

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

Ve iş gereksinimi değişti ve bir kullanıcının sahip olduğu tüm önceki adresleri ve telefon numaralarını kontrol etmemiz gerekiyordu. yeni şema buna benzer

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

Herhangi bir kullanıcının mevcut adresini bulmak için, DESC ve LIMIT 1 revizyonu ile UserData'yı ararız.

Bir kullanıcının adresini belirli bir süre arasında almak için created_on bewteen (tarih1, tarih 2) kullanabiliriz


Bu, sahip olmak istediğim ama bilmek istediğim bir çözümdür, tetik kullanarak bu tabloya id_user'ı nasıl ekleyebilirsiniz?
thecassion

1
Ne oldu revision=1ait id_user=1? Önce sayımınızın olduğunu düşünmüştüm 0,2,3,...ama sonra id_user=2revizyon sayımı için şunu gördüm0,1, ...
Pathros

1
İhtiyacınız yoktur idve id` (kullanıcı kimliği) ve id_usersütunları . . Just use a group ID of revision
Gajus

6

MariaDB, tam olarak istediğiniz şeyi yapan standart SQL özelliği olan 10.3'ten beri Sistem Sürümlendirmesini destekler: tablo kayıtlarının geçmişini depolar ve SELECTsorgular yoluyla ona erişim sağlar . MariaDB, MySQL'in açık geliştirme çatalı. Bu bağlantı aracılığıyla Sistem Sürümlendirme hakkında daha fazlasını bulabilirsiniz:

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


Lütfen yukarıdaki bağlantıdan aşağıdakilere dikkat edin: "mysqldump, sürümlü tablolardan geçmiş satırları okumaz ve bu nedenle geçmiş veriler yedeklenmez. Ayrıca, bir ekleme / ekleme ile tanımlanamayacakları için zaman damgalarının geri yüklenmesi mümkün olmaz Bir kullanıcı."
Daniel

4

Neden sadece bin günlük dosyalarını kullanmıyorsunuz? Çoğaltma Mysql sunucusunda ayarlanmışsa ve binlog dosya biçimi ROW olarak ayarlanmışsa, tüm değişiklikler yakalanabilir.

Noplay adlı iyi bir python kitaplığı kullanılabilir. Daha fazla bilgi burada .


2
Çoğaltmaya sahip olmasanız / ihtiyacınız olmasa bile Binlog kullanılabilir. Binlog'un birçok faydalı kullanım durumu vardır. Çoğaltma muhtemelen en yaygın kullanım durumudur, ancak burada belirtildiği gibi yedeklemeler ve denetim geçmişi için de kullanılabilir.
webaholik

3

Sadece 2 sentim. Geçici olayın çözümüne çok benzer şekilde, tam olarak neyin değiştiğini kaydeden bir çözüm yaratırdım.

Değişikliklerim Tablosu basit olacaktır:

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

1) Ana tabloda bir satırın tamamı değiştirildiğinde, bu tabloya çok sayıda girdi girecektir, ANCAK bu pek olası değildir, bu nedenle büyük bir sorun değildir (insanlar genellikle yalnızca bir şeyi değiştirirler) 2) Eski Değer (ve eğer istemek) herhangi bir veri olabileceği için bir tür epik "her tür" olmalıdır, bunu RAW türleriyle yapmanın veya içeri ve dışarı dönüştürmek için yalnızca JSON dizelerini kullanmanın bir yolu olabilir.

Minimum veri kullanımı, ihtiyacınız olan her şeyi depolar ve aynı anda tüm tablolar için kullanılabilir. Şu anda bunu kendim araştırıyorum, ancak bu benim gideceğim yol olabilir.

Oluşturma ve Silme için, yalnızca satır kimliği, alan gerekmez. Ana tablodaki bir bayrağı sildiğinizde (aktif mi?) İyi olur.


0

Bunu yapmanın doğrudan yolu, tablolarda tetikleyiciler oluşturmaktır. Bazı koşulları veya eşleme yöntemlerini ayarlayın. Güncelleme veya silme işlemi gerçekleştiğinde, otomatik olarak 'değişiklik' tablosuna eklenir.

Ama en büyük kısmı, eğer çok sayıda sütunumuz ve çok sayıda masamız varsa. Her tablonun her sütunun adını yazmalıyız. Açıkçası, bu zaman kaybı.

Bunu daha güzel halletmek için, sütunların adını almak için bazı prosedürler veya işlevler oluşturabiliriz.

Bunu yapmak için 3. bölüm aracını da kullanabiliriz. Burada bir java programı yazıyorum Mysql Tracker


Mysql İzleyicinizi nasıl kullanabilirim?
webchun

1
1. Her tabloda birincil anahtar olarak bir kimlik sütununuz olduğundan emin olun. 2. Java dosyasını yerel (veya IDE) 'ye kopyalayın 3. Kitaplıkları içe aktarın ve 9-15 satırlarındaki statik değişkenleri veritabanı yapılandırmanıza ve yapınıza göre düzenleyin. 4. Java dosyasını ayrıştırın ve çalıştırın 5. Konsol günlüğünü kopyalayın ve Mysql komutları olarak çalıştırın
goforu

create table like tablesanırım tüm sütunları kolayca kopyalar
Jonathan
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.