Có tùy chọn / tính năng MySQL để theo dõi lịch sử các thay đổi đối với bản ghi không?


122

Tôi đã được hỏi liệu tôi có thể theo dõi các thay đổi đối với các bản ghi trong cơ sở dữ liệu MySQL hay không. Vì vậy, khi một trường đã được thay đổi, trường cũ và mới sẽ có sẵn và ngày điều này diễn ra. Có một tính năng hoặc kỹ thuật phổ biến để làm điều này?

Nếu vậy, tôi đã nghĩ đến việc làm một cái gì đó như thế này. Tạo một bảng có tên changes. Nó sẽ bao gồm các lĩnh vực tương tự như các bậc thầy bảng nhưng bắt đầu bằng cũ và mới, nhưng chỉ dành cho những lĩnh vực mà thực sự đã được thay đổi và TIMESTAMPcho nó. Nó sẽ được lập chỉ mục với một ID. Bằng cách này, một SELECTbáo cáo có thể được chạy để hiển thị lịch sử của từng bản ghi. Đây có phải là một phương pháp tốt? Cảm ơn!

Câu trả lời:


83

Nó tinh tế.

Nếu yêu cầu nghiệp vụ là "Tôi muốn kiểm tra các thay đổi đối với dữ liệu - ai đã làm gì và khi nào?", Bạn thường có thể sử dụng bảng kiểm tra (theo ví dụ về trình kích hoạt mà Keethanjan đã đăng). Tôi không phải là một fan cuồng của các trình kích hoạt, nhưng nó có lợi ích lớn là việc triển khai tương đối dễ dàng - mã hiện tại của bạn không cần biết về các trình kích hoạt và công cụ kiểm tra.

Nếu yêu cầu kinh doanh là "cho tôi biết trạng thái của dữ liệu vào một ngày nhất định trong quá khứ", điều đó có nghĩa là khía cạnh thay đổi theo thời gian đã đi vào giải pháp của bạn. Trong khi bạn có thể xây dựng lại trạng thái của cơ sở dữ liệu chỉ bằng cách nhìn vào các bảng kiểm tra, nó rất khó và dễ xảy ra lỗi, và đối với bất kỳ logic cơ sở dữ liệu phức tạp nào, nó sẽ trở nên khó sử dụng. Ví dụ, nếu doanh nghiệp muốn biết "tìm địa chỉ của những bức thư mà lẽ ra chúng tôi phải gửi cho những khách hàng có hóa đơn chưa thanh toán, chưa thanh toán vào ngày đầu tiên của tháng", bạn có thể phải tra cứu hàng tá bảng kiểm toán.

Thay vào đó, bạn có thể đưa khái niệm thay đổi theo thời gian vào thiết kế lược đồ của mình (đây là tùy chọn thứ hai mà Keethanjan đề xuất). Đây là một thay đổi đối với ứng dụng của bạn, chắc chắn ở mức logic nghiệp vụ và mức độ bền bỉ, vì vậy nó không hề tầm thường.

Ví dụ: nếu bạn có một bảng như thế này:

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

và bạn muốn theo dõi theo thời gian, bạn sẽ sửa đổi nó như sau:

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

Mỗi khi bạn muốn thay đổi bản ghi khách hàng, thay vì cập nhật bản ghi, bạn đặt VALID_UNTIL trên bản ghi hiện tại thành NOW () và chèn một bản ghi mới với VALID_FROM (hiện tại) và VALID_UNTIL rỗng. Bạn đặt trạng thái "CUSTOMER_USER" thành ID đăng nhập của người dùng hiện tại (nếu bạn cần giữ điều đó). Nếu khách hàng cần được xóa, bạn sử dụng cờ CUSTOMER_STATUS để biểu thị điều này - bạn không bao giờ được xóa bản ghi khỏi bảng này.

Bằng cách đó, bạn luôn có thể tìm thấy trạng thái của bảng khách hàng trong một ngày nhất định - địa chỉ là gì? Họ đã đổi tên chưa? Bằng cách kết hợp với các bảng khác có ngày valid_from và valid_until tương tự, bạn có thể dựng lại toàn bộ bức tranh lịch sử. Để tìm trạng thái hiện tại, bạn tìm kiếm các bản ghi có ngày VALID_UNTIL rỗng.

Nó khó sử dụng (nói đúng ra, bạn không cần valid_from, nhưng nó làm cho các truy vấn dễ dàng hơn một chút). Nó làm phức tạp thiết kế của bạn và quyền truy cập cơ sở dữ liệu của bạn. Nhưng nó làm cho việc tái tạo thế giới dễ dàng hơn rất nhiều.


Nhưng nó sẽ thêm dữ liệu trùng lặp cho những trường không được cập nhật? Làm thế nào để quản lý nó?
itzmukeshy7 Ngày

Với cách tiếp cận thứ hai, vấn đề nảy sinh đối với việc tạo báo cáo nếu một hồ sơ khách hàng được chỉnh sửa trong một thời gian, rất khó để nhận ra liệu một mục nhập cụ thể thuộc về cùng một khách hàng hay khác.
Akshay Joshi

Đề xuất tốt nhất mà tôi chưa thấy cho vấn đề này
Worthy.

Ồ và để đáp lại các bình luận, bạn chỉ cần lưu trữ null cho mọi thứ khác không thay đổi thì sao? Vì vậy, phiên bản mới nhất sẽ là tất cả dữ liệu mới nhất, nhưng nếu tên từng là "Bob" cách đây 5 ngày, thì chỉ cần có một hàng, tên = bob và có giá trị đến 5 ngày trước.
Worthy

2
Sự kết hợp giữa customer_id và ngày tháng là khóa chính, vì vậy chúng sẽ được đảm bảo là duy nhất.
Neville Kuyt

186

Đây là một cách đơn giản để làm điều này:

Đầu tiên, hãy tạo một bảng lịch sử cho mỗi bảng dữ liệu bạn muốn theo dõi (truy vấn ví dụ bên dưới). Bảng này sẽ có một mục nhập cho mỗi truy vấn chèn, cập nhật và xóa được thực hiện trên mỗi hàng trong bảng dữ liệu.

Cấu trúc của bảng lịch sử sẽ giống như bảng dữ liệu mà nó theo dõi ngoại trừ ba cột bổ sung: một cột để lưu trữ thao tác đã xảy ra (chúng ta hãy gọi nó là 'hành động'), ngày và giờ của thao tác và một cột để lưu trữ một số thứ tự ('bản sửa đổi'), số này tăng lên mỗi thao tác và được nhóm theo cột khóa chính của bảng dữ liệu.

Để thực hiện hành vi trình tự này, một chỉ mục hai cột (tổng hợp) được tạo trên cột khóa chính và cột sửa đổi. Lưu ý rằng bạn chỉ có thể thực hiện trình tự theo cách này nếu công cụ được sử dụng bởi bảng lịch sử là MyISAM ( Xem 'Ghi chú MyISAM' trên trang này)

Bảng lịch sử khá dễ tạo. Trong truy vấn ALTER TABLE bên dưới (và trong các truy vấn kích hoạt bên dưới), hãy thay thế 'primary_key_column' bằng tên thực của cột đó trong bảng dữ liệu của bạn.

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

Và sau đó bạn tạo các trình kích hoạt:

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;

Và bạn đã hoàn thành. Bây giờ, tất cả các lần chèn, cập nhật và xóa trong 'MyDb.data' sẽ được ghi lại trong 'MyDb.data_history', cung cấp cho bạn một bảng lịch sử như thế này (trừ đi cột 'data_columns' có sẵn)

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 

Để hiển thị các thay đổi cho một cột nhất định hoặc các cột từ bản cập nhật đến bản cập nhật, bạn cần phải tham gia bảng lịch sử với chính nó trên khóa chính và cột trình tự. Bạn có thể tạo một dạng xem cho mục đích này, ví dụ:

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

Chỉnh sửa: Ồ wow, mọi người thích thứ bảng lịch sử của tôi từ 6 năm trước: P

Tôi cho rằng việc triển khai nó vẫn còn ì ạch, ngày càng lớn hơn và khó sử dụng hơn. Tôi đã viết các khung nhìn và giao diện người dùng khá đẹp để xem lịch sử trong cơ sở dữ liệu này, nhưng tôi không nghĩ rằng nó đã từng được sử dụng nhiều. Vì vậy, nó đi.

Để giải quyết một số nhận xét không theo thứ tự cụ thể:

  • Tôi đã thực hiện việc triển khai của riêng mình trong PHP có liên quan nhiều hơn một chút và tránh được một số vấn đề được mô tả trong nhận xét (đáng kể là có các chỉ mục được chuyển qua. Nếu bạn chuyển các chỉ mục duy nhất sang bảng lịch sử, mọi thứ sẽ bị hỏng. Có giải pháp cho này trong phần bình luận). Theo dõi bài đăng này đến bức thư có thể là một cuộc phiêu lưu, tùy thuộc vào cơ sở dữ liệu của bạn được thiết lập như thế nào.

  • Nếu mối quan hệ giữa khóa chính và cột sửa đổi dường như không có nghĩa là khóa tổng hợp bị khóa bằng cách nào đó. Trong một vài trường hợp hiếm hoi, tôi đã để điều này xảy ra và không hiểu nguyên nhân.

  • Tôi nhận thấy giải pháp này khá hiệu quả, sử dụng các trình kích hoạt như nó vốn có. Ngoài ra, MyISAM có tốc độ chèn nhanh, đó là tất cả những gì mà trình kích hoạt làm. Bạn có thể cải thiện điều này hơn nữa với lập chỉ mục thông minh (hoặc thiếu ...). Chèn một hàng vào bảng MyISAM với khóa chính không phải là một thao tác bạn cần tối ưu hóa, thực sự, trừ khi bạn gặp sự cố nghiêm trọng đang xảy ra ở nơi khác. Trong toàn bộ thời gian tôi đang chạy cơ sở dữ liệu MySQL, việc triển khai bảng lịch sử này đã được bật, nó không bao giờ là nguyên nhân của bất kỳ (nhiều) sự cố hiệu suất nào xuất hiện.

  • nếu bạn nhận được nhiều lần chèn, hãy kiểm tra lớp phần mềm của bạn để biết loại CHÈN BỎ QUA các truy vấn. Hrmm, không thể nhớ bây giờ, nhưng tôi nghĩ có vấn đề với lược đồ này và các giao dịch cuối cùng không thành công sau khi chạy nhiều hành động DML. Một cái gì đó cần được biết, ít nhất.

  • Điều quan trọng là các trường trong bảng lịch sử và bảng dữ liệu phải khớp với nhau. Hay nói đúng hơn là bảng dữ liệu của bạn không có NHIỀU cột hơn bảng lịch sử. Nếu không, các truy vấn chèn / update / del trên bảng dữ liệu sẽ không thành công, khi việc chèn vào bảng lịch sử đặt các cột trong truy vấn không tồn tại (do d. * Trong truy vấn trình kích hoạt) và trình kích hoạt không thành công. Sẽ thật tuyệt nếu MySQL có thứ gì đó giống như các trình kích hoạt giản đồ, nơi bạn có thể thay đổi bảng lịch sử nếu các cột được thêm vào bảng dữ liệu. MySQL hiện có điều đó không? Dạo này mình làm React: P


3
tôi thực sự thích giải pháp này. tuy nhiên nếu bảng chính của bạn không có khóa chính hoặc bạn không biết khóa chính là gì, thì nó hơi phức tạp.
Benjamin Eckstein

1
Gần đây tôi đã gặp sự cố khi sử dụng giải pháp này cho một dự án, vì cách tất cả các chỉ mục từ bảng gốc được sao chép vào bảng lịch sử (do cách TẠO BẢNG ... THÍCH .... hoạt động). Việc có các chỉ mục duy nhất trên bảng lịch sử có thể khiến truy vấn CHÈN trong trình kích hoạt SAU CẬP NHẬT thành barf, vì vậy chúng cần được xóa. Trong tập lệnh php mà tôi có thực hiện công việc này, tôi truy vấn bất kỳ chỉ mục duy nhất nào trên các bảng lịch sử mới được tạo (với "SHOW INDEX FROM data_table WHERE Key_name! = 'PRIMARY' và Non_unique = 0"), rồi xóa chúng.
đóng cửa tạm thời

3
Ở đây, chúng tôi luôn nhận được dữ liệu lặp lại được chèn vào bảng sao lưu. Giả sử nếu chúng ta có 10 trường trong một bảng và chúng ta đã cập nhật 2 thì chúng ta sẽ thêm dữ liệu lặp lại cho 8 trường còn lại. Làm thế nào để khắc phục từ nó?
itzmukeshy7 Ngày

6
Bạn có thể tránh vô tình mang theo trên các chỉ số khác nhau bằng cách thay đổi tạo ra bảng báo cáo đểCREATE TABLE MyDB.data_history as select * from MyDB.data limit 0;
Eric Hayes

4
@transientclosure, bạn sẽ đề xuất đưa các trường khác vào lịch sử như thế nào mà không phải là một phần của truy vấn ban đầu? ví dụ: tôi muốn theo dõi ai thực hiện những thay đổi đó. để chèn, nó đã có một ownertrường và để cập nhật, tôi có thể thêm một updatedbytrường, nhưng để xóa, tôi không chắc mình có thể thực hiện điều đó như thế nào thông qua trình kích hoạt. cập nhật data_historyhàng với id người dùng trong cảm thấy bẩn: P
Ngựa

16

Bạn có thể tạo trình kích hoạt để giải quyết vấn đề này. Đây là một hướng dẫn để làm như vậy (liên kết lưu trữ).

Đặt các ràng buộc và quy tắc trong cơ sở dữ liệu tốt hơn là viết mã đặc biệt để xử lý cùng một tác vụ vì nó sẽ ngăn nhà phát triển khác viết một truy vấn khác bỏ qua tất cả mã đặc biệt và có thể khiến cơ sở dữ liệu của bạn có tính toàn vẹn dữ liệu kém.

Trong một thời gian dài, tôi đã sao chép thông tin sang một bảng khác bằng tập lệnh vì MySQL không hỗ trợ trình kích hoạt vào thời điểm đó. Bây giờ tôi nhận thấy trình kích hoạt này hiệu quả hơn trong việc theo dõi mọi thứ.

Trình kích hoạt này sẽ sao chép giá trị cũ vào bảng lịch sử nếu nó bị thay đổi khi ai đó chỉnh sửa một hàng. Editor IDlast modđược lưu trữ trong bảng gốc mỗi khi ai đó chỉnh sửa hàng đó; thời gian tương ứng với thời điểm nó được thay đổi thành dạng hiện tại.

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

Một giải pháp khác là giữ trường Bản sửa đổi và cập nhật trường này khi lưu. Bạn có thể quyết định rằng giá trị tối đa là bản sửa đổi mới nhất hoặc 0 là hàng gần đây nhất. Tùy bạn.


9

Đây là cách chúng tôi giải quyết nó

bảng Người dùng trông như thế này

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

Và yêu cầu kinh doanh đã thay đổi và chúng tôi cần phải kiểm tra tất cả các địa chỉ và số điện thoại trước đây mà người dùng từng có. lược đồ mới trông như thế này

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

Để tìm địa chỉ hiện tại của bất kỳ người dùng nào, chúng tôi tìm kiếm Dữ liệu người dùng với bản sửa đổi DESC và LIMIT 1

Để lấy địa chỉ của người dùng trong một khoảng thời gian nhất định, chúng ta có thể sử dụng create_on beteen (date1, date 2)


Đó là một giải pháp mà tôi muốn có nhưng tôi muốn biết Làm thế nào bạn có thể chèn id_user vào bảng này bằng cách sử dụng trigger?
thecassion

1
Điều gì đã xảy ra với revision=1của id_user=1? Đầu tiên tôi nghĩ rằng số đếm của bạn là 0,2,3,...nhưng sau đó tôi thấy rằng id_user=2số đếm của bản sửa đổi là0,1, ...
Pathros

1
Bạn không cần idid_usercột . Just use a group ID of id` (ID người dùng) và revision.
Gajus

6

MariaDB hỗ trợ Phiên bản hệ thống kể từ 10.3, đây là tính năng SQL tiêu chuẩn thực hiện chính xác những gì bạn muốn: nó lưu trữ lịch sử các bản ghi bảng và cung cấp quyền truy cập vào nó thông qua SELECT các truy vấn. MariaDB là một nhánh phát triển mở của MySQL. Bạn có thể tìm thêm thông tin về Phiên bản hệ thống của nó qua liên kết này:

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


Vui lòng lưu ý những điều sau từ liên kết ở trên: "mysqldump không đọc các hàng lịch sử từ các bảng được tạo phiên bản và do đó, dữ liệu lịch sử sẽ không được sao lưu. Ngoài ra, sẽ không thể khôi phục các dấu thời gian vì chúng không thể được xác định bằng chèn / một người dùng."
Daniel

4

Tại sao không chỉ sử dụng tệp nhật ký bin? Nếu bản sao được đặt trên máy chủ Mysql và định dạng tệp binlog được đặt thành ROW, thì tất cả các thay đổi có thể được ghi lại.

Có thể sử dụng một thư viện python tốt được gọi là noplay. Thêm thông tin ở đây .


2
Binlog có thể được sử dụng ngay cả khi bạn không có / cần sao chép. Binlog có nhiều trường hợp sử dụng có lợi. Sao chép có lẽ là trường hợp sử dụng phổ biến nhất, nhưng nó cũng có thể được tận dụng để sao lưu và lịch sử kiểm tra, như đã đề cập ở đây.
webaholik

3

Chỉ 2 xu của tôi. Tôi sẽ tạo ra một giải pháp ghi lại chính xác những gì đã thay đổi, rất giống với giải pháp của tạm thời.

ChangesTable của tôi sẽ đơn giản là:

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

1) Khi toàn bộ hàng được thay đổi trong bảng chính, rất nhiều mục nhập sẽ đi vào bảng này, NHƯNG điều đó rất khó xảy ra, vì vậy không phải là vấn đề lớn (mọi người thường chỉ thay đổi một thứ) 2) OldVaue (và NewValue nếu bạn muốn) phải là một loại "bất kỳ loại nào" sử thi nào đó vì nó có thể là bất kỳ dữ liệu nào, có thể có cách để thực hiện điều này với các loại RAW hoặc chỉ sử dụng chuỗi JSON để chuyển đổi trong và ngoài.

Sử dụng dữ liệu tối thiểu, lưu trữ mọi thứ bạn cần và có thể được sử dụng cho tất cả các bảng cùng một lúc. Tôi đang tự nghiên cứu vấn đề này ngay bây giờ, nhưng đây có thể là cách tôi đi.

Đối với Tạo và Xóa, chỉ cần ID hàng, không cần trường. Khi xóa một cờ trên bảng chính (hoạt động?) Sẽ tốt.


0

Cách trực tiếp của việc này là tạo các trình kích hoạt trên các bảng. Đặt một số điều kiện hoặc phương pháp ánh xạ. Khi cập nhật hoặc xóa xảy ra, nó sẽ tự động chèn vào bảng 'thay đổi'.

Nhưng phần lớn nhất là nếu chúng ta có nhiều cột và nhiều bảng. Chúng ta phải gõ tên từng cột của mọi bảng. Rõ ràng, đó là lãng phí thời gian.

Để xử lý điều này một cách rõ ràng hơn, chúng ta có thể tạo một số thủ tục hoặc hàm để lấy tên của các cột.

Chúng ta cũng có thể sử dụng công cụ phần thứ 3 để thực hiện việc này. Ở đây, tôi viết một chương trình java Mysql Tracker


làm cách nào để sử dụng Mysql Tracker của bạn?
webchun

1
1. Đảm bảo rằng bạn có một cột id làm khóa chính trong mỗi bảng. 2. Sao chép tệp java vào cục bộ (hoặc IDE) 3. Nhập lib và chỉnh sửa các biến tĩnh từ dòng 9-15 theo cấu hình và cấu trúc cơ sở dữ liệu của bạn. 4. Parse và chạy file java 5. Sao chép giao diện điều khiển đăng nhập và thực hiện nó như lệnh Mysql
goforu

create table like tabletôi nghĩ sao chép tất cả các cột một cách dễ dàng
Jonathan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.