Tối ưu hóa chương trình C# – Cơ sở dữ liệu

Trong bài viết trước đây tôi đã giới thiệu một số kĩ thuật tối ưu trong C# với các lớp và phương thức căn bản. Trong bài viết này, tôi chú trọng đến việc tối ưu chương trình trong quá trình làm việc với cơ sở dữ liệu. Đây là điều khá quan trọng đối với bất kì lập trình viên nào bởi vì hầu hết đều xem đây là trọng tâm cũng như để làm việc sau này.

1.   Nạp dữ liệu – DataSet và DataReader

Có một sự khác biệt dễ nhận thấy khi bạn thao tác với những loại cơ sở dữ liệu khác nhau. Chẳng hạn thử nghiệm trên SQL Server và Access thì tốc độ thực thi trên SQL Server sẽ nhanh hơn, sự khác biệt giữa các cách xử lý dữ liệu cũng nhiều hơn.

Trong minh họa này, tôi sẽ sử dụng SQL Server để nạp dữ liệu từ một bảng và lặp qua từng dòng để thêm dữ liệu của một cột nào đó vào tập hợp List<string>. Cách đầu tiên sử dụng một DataSet để lưu dữ liệu và cách thứ hai sử dụng một SqlDataReader để đọc dữ liệu.

Ở đây tôi sử dụng lớp SqlAccess với hai phương thức chính để lấy dữ liệu từ cơ sở dữ liệu SQL Server thông qua DataSet và reader. Nội dung của chúng như sau:

public DataSet GetDataSet(string tableName)
{
string strQuery= “select * from ” + tableName;
SqlCommand cmd = new SqlCommand(strQuery,conn);
SqlDataAdapter da = new SqlDataAdapter(cmd);
DataSet ds = new DataSet();
da.Fill(ds, tableName);
return ds;
}
public SqlDataReader GetReader(string tableName)
{
string strQuery = “select * from ” + tableName;
SqlCommand cmd = new SqlCommand(strQuery,conn);
return cmd.ExecuteReader();
}

Đoạn mã 1 – Sử dụng DataSet:

Listlist = new List();
DataSet ds= sqlAccess.GetDataSet(“tblUser”);
foreach(DataRow row in ds.Tables[0].Rows)
list.Add(row[“Alias”].ToString());

Đoạn mã 2 – Sử dụng SqlDataReader:

Listlist = new List();
SqlDataReader reader = sqlAccess.GetReader(“tblUser”);
while (reader.Read())
list.Add(reader[“Alias”].ToString());
reader.Dispose();

Theo kết quả thử nghiệm thì tốc độ của SqlDataReader nhanh gấp 2 lần so với khi sử dụng DataSet. Chính vì thế trong những trường hợp không cần phải lưu dữ liệu tạm thời để làm việc, bạn nên tránh sử dụng DataSet để nạp dữ liệu. Việc sử dụng reader mặc dù có nhiều hạn chế hơn DataSet như chỉ có thể đọc dữ liệu, chỉ đi tới theo kiểu “thà chết chớ lui” nhưng trong nhiều trường hợp, bạn cũng cần đánh đổi những chức năng đó để đạt được tốc độ cần thiết cho ứng dụng của mình.
Nếu trong hai ví dụ trên bạn sử dụng kết nối đến tập tin Access thì tốc độ sẽ chậm hơn đồng thời sự khác biệt giữa tốc độ thực thi của cách dùng DataSet với OleDbDataReader sẽ chỉ khoảng 1.5 (nghiêng về phía reader).
2. Thuộc tính CommandType của đối tượng Command
Có một sự khác biệt khi chọn lựa giá trị cho thuộc tính CommandType của đối tượng Command. Mặc định thì CommandType là Text tức là dựa trên câu lệnh Sql để thực thi lệnh. Tuy nhiên nếu chuyển CommandType sang kiểu StoredProcedure hoặc TableDirect thì tốc độ sẽ nhanh hơn.
Đáng tiếc là không thể sử dụng kiểu CommandType.TableDirect khi kết nối đến SQL Server, chính vì thể tôi sẽ sử dụng đối tượng OleDbDataReader để kết nối và đọc 1 bảng dữ liệu từ file Access. Các phương thức sau lần lượt sử dụng từng giá trị CommandType khác nhau để cùng lấy dữ liệu từ bảng SinhVien:
Phương thức 1 – Sử dụng CommandType.Text

public OleDbDataReader GetReader_Text()
{
string strQuery = “select * from SinhVien”;
OleDbCommand cmd = new OleDbCommand(strQuery,con);
return cmd.ExecuteReader();
}

Phương thức 2 – Sử dụng CommandType.StoredProcedure

public OleDbDataReader GetReader_StoredProcedure()
{
OleDbCommand cmd = new OleDbCommand(“qryGetUsers”,con);
cmd.CommandType = CommandType.StoredProcedure;
return cmd.ExecuteReader();
}

Phương thức 3 – Sử dụng CommandType.TableDirect

public OleDbDataReader GetReader_TableDirect()
{
OleDbCommand cmd = new OleDbCommand(“SinhVien”, con);
cmd.CommandType = CommandType.TableDirect;
return cmd.ExecuteReader();
}

Kết quả sau khi thực thi 3 phương thức này như sau (tốc độ hoàn thành tác vụ):

– CommandType.Text: 2618

– CommandType.StoredProcedure: 2444

– CommandType.TableDirect 1231

Như bạn có thể thấy là kiểu TableDirect có tốc độ thực thi nhanh nhất. Cách này rất thích hợp trong trường hợp bạn chỉ cần nạp một bảng dữ liệu đơn lẻ, đồng thời thao tác viết lệnh cũng nhanh hơn. Đối với những trường hợp nạp dữ liệu phức tạp hoặc để thực thi các lệnh Insert, Delete, Update,… thì hãy sử dụng StoredProcedure bất cứ khi nào có thể.
Theo Microsoft thì Stored Procedure có thể được thực thi mà không cần phải qua bước thông dịch, đồng thời giúp làm giảm lượng dữ liệu truyền tải giữa client và server trong các ứng dụng liên quan đến mạng.
3. Chỉ nạp những cột dữ liệu cần thiết
Điều này có lẽ ai cũng có thể hiểu, việc truy vấn dữ liệu với số cột ít hơn sẽ đồng nghĩa với lượng dữ liệu trả về ít và tốc độ cũng nhanh hơn. Một số lập trình viên do thói quen thường sử dụng dấu ‘*’ để nạp dữ liệu ngay cả trong trường hợp họ chỉ cần 1 đến 2 trường là đủ. Sự khác biệt về tốc độ này phụ thuộc vào số cột mà bạn muốn truy vấn nên sẽ không có giá trị so sánh nào trong phần này.
4. Tìm kiếm dữ liệu – DataTable và DataView
Một chức năng không thể thiếu trong các ứng dụng có kết nối cơ sở dữ liệu đó là tìm kiếm. Ở đây ta chỉ so sánh hai phương pháp tìm kiếm được hỗ trợ trong đối tượng DataTable và DataView.
Để sử dụng được phương thức Find này, điều cần làm là phải xác định tập khóa chính cho bảng. Giả sử tôi đọc dữ liệu từ bảng tblUser có cột Alias có các giá trị không trùng nhau, vì thế tôi có thể sử dụng cột này làm khóa chính.

DataTable table = sqlAccess.GetDataSet(“tblUser”).Tables[0];
table.PrimaryKey = new DataColumn[] { table.Columns[“Alias”] };

Đối với DataView thì “yêu sách” hơn, tức là để tìm kiếm được ta cần phải làm một bước nữa là sắp xếp cho nó.

DataTable table = sqlAccess.GetDataSet(“tblUser”).Tables[0];
table.PrimaryKey = new DataColumn[] { table.Columns[“Alias”] };
DataView dv = table.DefaultView;
dv.Sort = “Alias”;

Ta sẽ sử dụng hai phương thức sau đây để kiểm tra tốc độ của hai phương pháp này:
Phương thức 1 – Tìm kiếm bằng DataTable

long Test1()
{
DataTable table = sqlAccess.GetDataSet(“tblUser”).Tables[0];
table.PrimaryKey = new DataColumn[] { table.Columns[“Alias”] };
string email = “”;
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < 100000; i++)
{
DataRow row = table.Rows.Find(“YinYang”);
email = row[“Email”].ToString();
}
sw.Stop();
return sw.ElapsedMilliseconds;
}

Phương thức 2 – Tìm kiếm bằng DataView

long Test2()
{
DataTable table = sqlAccess.GetDataSet(“tblUser”).Tables[0];
table.PrimaryKey = new DataColumn[] { table.Columns[“Alias”] };
DataView dv = table.DefaultView;
dv.Sort = “Alias”;
string email = “”;
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < 100000; i++)
{
int index = dv.Find(“YinYang”);
email = dv[index].Row[“Email”].ToString();
}
sw.Stop();
return sw.ElapsedMilliseconds;
}

Quá trình hai phương thức trên không xảy ra bất kì ngoại lệ nào, có nghĩa là cả hai phương thức Find() đều tìm thấy dữ liệu trong bảng. Tuy nhiên xét về tốc độ thì phương thức 2 nhanh hơn khoảng 1.5 lần.
5. Lọc dữ liệu – DataTable và DataView
Bên cạnh tìm kiếm thì lọc dữ liệu cũng là một chức năng rất quan trọng trong nhiều trường hợp như tìm kiếm kết quả tương đối, hiển thị dữ liệu theo từng mục chọn,… Cả DataTable và DataView đều có phương pháp để lọc dữ liệu, tuy nhiên cách sử dụng không giống nhau. Để lọc bằng DataTable, ta sử dụng phương thức Select(), còn đối với DataView thì phải gán biểu thức lọc cho thuộc tính RowFilter
Đối với phương thức Select() thì DataTable trả về một mảng các đối tượng DataRow, trong khi đó, DataView thay đổi lại các phần tử của nó dựa vào biểu thức lọc với dữ liệu được lưu trong thuộc tính Table của nó.
Xét hai phương thức sau:
Phương thức 1 – Lọc bằng DataTable

long Test1()
{
DataTable table = sqlAccess.GetDataSet(“tblUser”).Tables[0];
string email = “”
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < 100000; i++)
{
DataRow[] row = table.Select(“Alias like ‘Yin%’”);
email = row[0][“Email”].ToString();
}
sw.Stop();
return sw.ElapsedMilliseconds;
}

Phương thức 2 – Lọc bằng DataView

long Test2()
{
DataTable table = sqlAccess.GetDataSet(“tblUser”).Tables[0];
DataView dv = table.DefaultView;
string email = “”;
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < 100000; i++)
{
dv.RowFilter = “Alias like ‘Yin%’”;
email = dv[0].Row[“Email”].ToString();
}
sw.Stop();
return sw.ElapsedMilliseconds;
}

Khi chạy thử thì phương thức 1 chiếm thời gian lâu hơn khoảng 100 lần so với phương thức 2. Ví dụ như đối với cơ sở dữ liệu của tôi thì kết quả là (tương đối):
-Phương thức 1 mất 6000ms (mili giây)
-Phương thức 2 mất 60ms
Nếu như thay biểu thức lọc trên với biểu thức lọc chính xác hơn bằng cách sử dụng dấu “=” thì tốc độ sẽ nhanh hơn. Ví dụ ta sửa lại biểu thức lọc ở hai ví dụ trên thành:
Alias = ‘YinYang’
Thì kết quả là:
-Phương thức 1 mất 1900ms
-Phương thức 2 mất 55ms
Khoảng cách về tốc độ giữa hai phương thức được rút ngắn lại còn khoảng 34.5 lần.
Dĩ nhiên trong thực tế khi giải quyết các vấn đề về lọc thì người ta thường sử dụng DataView. Còn phương thức Select() của DataTable thì giống như một kiểu truy vấn đơn giản để lấy về những dòng thích hợp cho mục đích nào đó. Ngoài ra Select() còn dùng để chuyển nhanh một DataTable thành một mảng các DataRow, sắp xếp, và còn có thể lọc để lấy ra những dòng có trạng thái đặc biệt như các dòng Deleted, Added, Unchanged,…thông qua một tham số kiểu enum DataViewRowState. Bạn hãy coi thử 4 overload của phương thức này để hiểu rõ hơn cách sử dụng.
6. Kết luận
Trên đây là những kinh nghiệm đơn giản để tăng hiệu suất của ứng dụng khi làm việc với cơ sở dữ liệu. Còn những chức năng như thêm, xóa,… dữ liệu thì bạn có thể tự mình thử nghiệm và so sánh. Những ví dụ kiểm tra trên chỉ mang tính chất tương đối khó có thể đúng cho tất cả trường hợp, nhưng cũng cho chúng ta thấy cái nhìn tổng quan để lựa chọn phương pháp nào phù hợp cho từng mục đích cụ thể.