Memakai Full-Text Search Di MySQL Server

Hari ini adalah hari pertama di tahun 2015. Sama seperti hari-hari sebelumnya, awan gelap dan angin kencang tidak kunjung hilang. Tidak ada yang lebih indah daripada mengawali hari pertama di tahun baru dengan segelas teh hangat sambil menulis blog. Pada artikel Memahami Eksekusi SQL di MySQL Server, saya menunjukkan bahwa query dengan pencarian floating seperti LIKE '%abc%' tidak dapat memanfaatkan index. Untuk mengatasi permasalahan tersebut, MySQL Server sejak versi 5.6.4 telah menambahkan dukungan Full-Text Index pada engine InnoDB. Full-Text Search memiliki kemampuan yang jauh lebih canggih dibandingkan dengan floating LIKE: ia akan mengurutkan record berdasarkan hasil yang paling relevan (misalnya record yang mengandung banyak jumlah kata yang dicari akan muncul di urutan awal).

Sebagai contoh, saya dapat menambahkan Full-Text Index pada kolom nama di tabel barang dengan memberikan SQL seperti berikut ini:

ALTER TABLE barang
ADD FULLTEXT INDEX idx_nama(nama ASC);

Setelah ini, saya dapat memberikan SQL yang menggunakan Full-Text Search pada kolom nama seperti:

SELECT nama FROM barang WHERE MATCH(nama) AGAINST('seal');

Query di atas akan mengembalikan seluruh nama produk yang mengandung kata 'seal'. Untuk menunjukkan bahwa index dipakai, saya dapat menggunakan EXPLAIN yang hasilnya terlihat seperti pada gambar berikut ini:

Full-Text Index Dipakai Pada Full-Text Search

Full-Text Index Dipakai Pada Full-Text Search

Agar lebih jelas, saya juga akan membandingkan perubahan kinerja yang dicapai. Saya akan mulai dengan memantau kinerja versi query yang memakai pencarian floating LIKE:

mysql> SET PROFILING=1;

mysql> SELECT SQL_NO_CACHE * FROM barang WHERE nama LIKE '%seal%';

mysql> SHOW PROFILE FOR QUERY 1;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000108 |
| checking permissions | 0.000009 |
| Opening tables       | 0.000715 |
| init                 | 0.000056 |
| System lock          | 0.000009 |
| optimizing           | 0.000014 |
| statistics           | 0.000037 |
| preparing            | 0.000022 |
| executing            | 0.000003 |
| Sending data         | 0.154063 |
| end                  | 0.000014 |
| query end            | 0.000010 |
| closing tables       | 0.000017 |
| freeing items        | 0.000131 |
| cleaning up          | 0.000031 |
+----------------------+----------+

mysql> SELECT * FROM barang WHERE nama LIKE '%seal%';

mysql> SHOW PROFILE FOR QUERY 2;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000069 |
| checking permissions | 0.000008 |
| Opening tables       | 0.000024 |
| init                 | 0.000032 |
| System lock          | 0.000010 |
| optimizing           | 0.000009 |
| statistics           | 0.000018 |
| preparing            | 0.000014 |
| executing            | 0.000004 |
| Sending data         | 0.009502 |
| end                  | 0.000013 |
| query end            | 0.000009 |
| closing tables       | 0.000014 |
| freeing items        | 0.000122 |
| cleaning up          | 0.000017 |
+----------------------+----------+

mysql> SELECT * FROM barang WHERE nama LIKE '%seal%';

mysql> SELECT * FROM barang WHERE nama LIKE '%seal%';

mysql> SELECT * FROM barang WHERE nama LIKE '%seal%';

mysql> SHOW PROFILES;
+----------+------------+------------------------------------------------------------+
| Query_ID | Duration   | Query                                                      |
+----------+------------+------------------------------------------------------------+
|        1 | 0.15523725 | SELECT SQL_NO_CACHE * FROM barang WHERE nama LIKE '%seal%' |
|        2 | 0.00986225 | SELECT SQL_NO_CACHE * FROM barang WHERE nama LIKE '%seal%' |
|        3 | 0.01032150 | SELECT SQL_NO_CACHE * FROM barang WHERE nama LIKE '%seal%' |
|        4 | 0.00977775 | SELECT SQL_NO_CACHE * FROM barang WHERE nama LIKE '%seal%' |
|        5 | 0.00983650 | SELECT SQL_NO_CACHE * FROM barang WHERE nama LIKE '%seal%' |
+----------+------------+------------------------------------------------------------+

Sekarang, saya akan membandingkannya dengan query yang memakai Full-Text Search:

mysql> SET PROFILING=1;

mysql> SELECT SQL_NO_CACHE * FROM barang WHERE MATCH(nama) AGAINST('seal');

mysql> SHOW PROFILE FOR QUERY 1;
+-------------------------+----------+
| Status                  | Duration |
+-------------------------+----------+
| starting                | 0.000115 |
| checking permissions    | 0.000008 |
| Opening tables          | 0.000708 |
| init                    | 0.000046 |
| System lock             | 0.000010 |
| optimizing              | 0.000013 |
| statistics              | 0.000037 |
| preparing               | 0.000013 |
| FULLTEXT initialization | 0.172336 |
| executing               | 0.000012 |
| Sending data            | 0.093124 |
| end                     | 0.000013 |
| query end               | 0.000013 |
| closing tables          | 0.000017 |
| freeing items           | 0.000150 |
| cleaning up             | 0.000022 |
+-------------------------+----------+

mysql> SELECT SQL_NO_CACHE * FROM barang WHERE MATCH(nama) AGAINST('seal');

mysql> SHOW PROFILE FOR QUERY 2;
+-------------------------+----------+
| Status                  | Duration |
+-------------------------+----------+
| starting                | 0.000090 |
| checking permissions    | 0.000014 |
| Opening tables          | 0.000027 |
| init                    | 0.000031 |
| System lock             | 0.000012 |
| optimizing              | 0.000009 |
| statistics              | 0.000022 |
| preparing               | 0.000010 |
| FULLTEXT initialization | 0.000320 |
| executing               | 0.000006 |
| Sending data            | 0.001038 |
| end                     | 0.000009 |
| query end               | 0.000009 |
| closing tables          | 0.000013 |
| freeing items           | 0.000141 |
| cleaning up             | 0.000018 |
+-------------------------+----------+

mysql> SELECT SQL_NO_CACHE * FROM barang WHERE MATCH(nama) AGAINST('seal');

mysql> SELECT SQL_NO_CACHE * FROM barang WHERE MATCH(nama) AGAINST('seal');

mysql> SELECT SQL_NO_CACHE * FROM barang WHERE MATCH(nama) AGAINST('seal');

mysql> SHOW PROFILES;
+----------+------------+---------------------------------------------------------------------+
| Query_ID | Duration   | Query                                                               |
+----------+------------+---------------------------------------------------------------------+
|        1 | 0.26663350 | SELECT SQL_NO_CACHE * FROM barang WHERE MATCH(nama) AGAINST('seal') |
|        2 | 0.00176875 | SELECT SQL_NO_CACHE * FROM barang WHERE MATCH(nama) AGAINST('seal') |
|        3 | 0.00201450 | SELECT SQL_NO_CACHE * FROM barang WHERE MATCH(nama) AGAINST('seal') |
|        4 | 0.00165800 | SELECT SQL_NO_CACHE * FROM barang WHERE MATCH(nama) AGAINST('seal') |
|        5 | 0.00160050 | SELECT SQL_NO_CACHE * FROM barang WHERE MATCH(nama) AGAINST('seal') |
+----------+------------+---------------------------------------------------------------------+

Pada hasil percobaan sederhana tersebut, terlihat bahwa bila saya mengabaikan eksekusi pertama yang lambat, maka untuk setiap query berikutnya, versi yang memakai Full-Text Search rata-rata lebih cepat 82% dibanding versi yang memakai float LIKE.

Selain melakukan pencarian pada modus Natural Language, Full-Text Search juga menyediakan modus Boolean. Sebagai contoh, saya bisa mencari kolom nama yang mengandung kata 'Seal' dan 'Set' tetapi tidak mengandung kata 'Shock' dengan query seperti berikut ini:

SELECT 
    nama
FROM
    barang
WHERE
    MATCH (nama) AGAINST ('+Seal +Set -Shock' IN BOOLEAN MODE);

Pada saat melakukan pencarian pada modus boolean, saya dapat memakai operator seperti + untuk menyertakan sebuah kata, - untuk memastikan sebuah kata tidak muncul, ( dan ) untuk pengelompokkan, dan sebagainya. Saya juga dapat menggunakan operator > dan < untuk memberikan prioritas kata yang dicari sehingga mempengaruhi urutan record yang dikembalikan. Sebagai contoh, query berikut ini:

SELECT 
    nama
FROM
    barang
WHERE
    MATCH (nama) AGAINST ('+Seal >Water' IN BOOLEAN MODE);

akan menghasilkan record yang mengandung kata 'Water' pada urutan yang lebih awal.

Full-Text Search juga memiliki modus Query Expansion yang akan melakukan pencarian dua kali. Pencarian kedua akan menghasilkan record yang kira-kira berhubungan dengan hasil pencarian pertama. Modus pencarian ini biasanya menghasilkan banyak record yang tidak relevan, tetapi bisa berguna untuk pencarian fuzzy dimana pengguna tidak tahu persis apa yang hendak dicari. Sebagai contoh, berikut adalah SQL yang memakai Query Expansion:

SELECT 
    nama
FROM
    barang
WHERE
    MATCH (nama) AGAINST ('seal water' WITH QUERY EXPANSION);

Bila saya mengerjakan query di atas, saya tidak hanya memperoleh nama yang mengandung 'water seal', tetapi juga yang berkaitan dengannya seperti 'water pump'. Pada modus Natural Language dan Boolean, saya hanya memperoleh 1 record yang benar-benar mengandung nama 'water seal'. Akan tetapi pada modus Query Expansion, saya bisa memperoleh hingga 92 record.

Full-Text Search akan mengabaikan stopword (kata yang dianggap tidak penting) pada kolom yang dicari. Secara default, stopword bawaan MySQL Server adalah:

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD;
+-------+
| value |
+-------+
| a     |
| about |
| an    |
| are   |
| as    |
| at    |
| be    |
| by    |
| com   |
| de    |
| en    |
| for   |
| from  |
| how   |
| i     |
| in    |
| is    |
| it    |
| la    |
| of    |
| on    |
| or    |
| that  |
| the   |
| this  |
| to    |
| was   |
| what  |
| when  |
| where |
| who   |
| will  |
| with  |
| und   |
| the   |
| www   |
+-------+

Terlihat bahwa stopword terdiri atas kata-kata dalam bahasa Inggris. Bila ingin memakai tabel lain sebagai daftar stopword, saya dapat mengubah nilai variabel innodb_ft_server_stopword_table untuk merujuk pada tabel lain tersebut.

Iklan

Memahami eksekusi SQL di MySQL Server

Tulisan ini akan menjadi tulisan paling akhir di tahun 2014. Sudah tiba saatnya untuk mengucapkan selamat tinggal pada 2014 dan bersiap-siap menyongsong 2015. Happy new year! Sembari menunggu tahun baru tiba, saya akan memeriksa slow query log di MySQL Server Server.

MySQL Server selalu mencatat query lambat yang pernah dikerjakannya pada slow query log yang memiliki nama dengan format nama_komputer-slow.log. Sebagai contoh, bila nama komputer saya adalah PC-Snake, maka saya dapat menemukan slow query log di lokasi C:\ProgramData\MySQL\MySQL Server 5.6\data\PC-Snake-slow.log. Lalu, apa kriteria untuk sebuah query yang dianggap lambat sehingga perlu dicatat di slow query log? Hal ini tergantung pada nilai variabel long_query_time yang default-nya adalah 10 detik dan nilai min_examined_row_limit yang default-nya adalah 0. Dengan demikian, secara default, query yang eksekusinya memakan waktu lebih dari 10 detik akan dicatat di slow query log.

Sebagai contoh, saya menemukan sebuah query lambat yang sering terjadi:

SELECT DISTINCT
    produk0_.id AS id1_39_0_,
    daftarstok1_.id AS id1_52_1_,
    listperiod2_.id AS id1_33_2_,
    produk0_.createdBy AS createdB2_39_0_,
    produk0_.createdDate AS createdD3_39_0_,
    produk0_.deleted AS deleted4_39_0_,
    produk0_.hargaDalamKota AS hargaDal5_39_0_,
    produk0_.hargaLuarKota AS hargaLua6_39_0_,
    produk0_.jumlah AS jumlah7_39_0_,
    produk0_.jumlahAkanDikirim AS jumlahAk8_39_0_,
    produk0_.jumlahRetur AS jumlahRe9_39_0_,
    produk0_.jumlahTukar AS jumlahT10_39_0_,
    produk0_.keterangan AS keteran11_39_0_,
    produk0_.levelMinimum AS levelMi12_39_0_,
    produk0_.modifiedBy AS modifie13_39_0_,
    produk0_.modifiedDate AS modifie14_39_0_,
    produk0_.nama AS nama15_39_0_,
    produk0_.poin AS poin16_39_0_,
    produk0_.satuan_id AS satuan_17_39_0_,
    produk0_.supplier_id AS supplie18_39_0_,
    daftarstok1_.jumlah AS jumlah2_52_1_,
    daftarstok1_.createdBy AS createdB3_52_1_,
    daftarstok1_.createdDate AS createdD4_52_1_,
    daftarstok1_.deleted AS deleted5_52_1_,
    daftarstok1_.gudang_id AS gudang_i8_52_1_,
    daftarstok1_.modifiedBy AS modified6_52_1_,
    daftarstok1_.modifiedDate AS modified7_52_1_,
    daftarstok1_.produk_id AS produk_i9_52_1_,
    daftarstok1_.produk_id AS produk_i9_39_0__,
    daftarstok1_.id AS id1_52_0__,
    daftarstok1_.gudang_id AS formula0_0__,
    listperiod2_.arsip AS arsip2_33_2_,
    listperiod2_.jumlah AS jumlah3_33_2_,
    listperiod2_.saldo AS saldo4_33_2_,
    listperiod2_.tanggalMulai AS tanggalM5_33_2_,
    listperiod2_.tanggalSelesai AS tanggalS6_33_2_,
    listperiod2_.createdBy AS createdB7_33_2_,
    listperiod2_.createdDate AS createdD8_33_2_,
    listperiod2_.deleted AS deleted9_33_2_,
    listperiod2_.modifiedBy AS modifie10_33_2_,
    listperiod2_.modifiedDate AS modifie11_33_2_,
    listperiod2_.riwayat_id AS riwayat12_52_1__,
    listperiod2_.id AS id1_33_1__,
    listperiod2_.listPeriodeRiwayat_ORDER AS listPer13_1__
FROM
    Produk produk0_
        LEFT OUTER JOIN
    StokProduk daftarstok1_ ON produk0_.id = daftarstok1_.produk_id
        LEFT OUTER JOIN
    PeriodeItemStok listperiod2_ ON daftarstok1_.id = listperiod2_.riwayat_id
WHERE
    1 = 1
ORDER BY produk0_.nama ASC;

Terlihat kompleks? Tenang saja, saya tidak menulis SQL tersebut secara manual melainkan memakai Hibernate JPA untuk menghasilkan query secara otomatis. Pada domain class, saya memiliki hierarki composition (whole-part relationship) berupa Produk memiliki StokProduk yang selanjutnya memiliki PeriodeItemStok. Karena MySQL Server adalah database relasional yang tidak mendukung composition, maka ia akan diterjemahkan menjadi LEFT JOIN. Ini adalah apa yang disebut sebagai memakai OOP sebagai paradigma tetapi menerapkan murni dalam bentuk relasional (Taniar, Pardede & Rahayu (2005), Composition in Object-Relational Database, http://www.irma-international.org/viewtitle/14285/).

Untuk mendapatkan bayangan bagaimana MySQL Server akan mengerjakan query diatas, saya dapat menggunakan EXPLAIN untuk melihat bagaimana ‘pola pikir’ MySQL dalam menjalankan query. Untuk itu, saya perlu menambahkan EXPLAIN sebelum SELECT seperti pada query berikut ini:

mysql> EXPLAIN SELECT ... /G

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: produk0_
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 443
        Extra: Using temporary; Using filesort
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: daftarstok1_
         type: ref
possible_keys: FK_qdv4fq1uprbpkool88p9y6h44
          key: FK_qdv4fq1uprbpkool88p9y6h44
      key_len: 8
          ref: inventory.produk0_.id
         rows: 1
        Extra: NULL
*************************** 3. row ***************************
           id: 1
  select_type: SIMPLE
        table: listperiod2_
         type: ref
possible_keys: FK_ls70le3nhwwxfw0gudwcpv4l3
          key: FK_ls70le3nhwwxfw0gudwcpv4l3
      key_len: 9
          ref: inventory.daftarstok1_.id
         rows: 1
        Extra: NULL
3 rows in set (0.00 sec)

Bagi yang tidak suka membaca informasi dalam bentuk tabel, MySQL Workbench dapat menyajikan hasil EXPLAIN dalam bentuk visual (secara default) seperti pada gambar berikut ini:

Hasil EXPLAIN dalam bentuk visual

Hasil EXPLAIN dalam bentuk visual

Pada tampilan visual di atas, saya perlu menghindari kotak merah yang disebut Full Table Scan. Ini adalah operasi yang paling berat karena harus mencari isi tabel satu per satu tanpa memakai index sama sekali.

Bila saya membiarkan pointer mouse agak lama di kotak tersebut, saya akan memperoleh informasi lebih lanjut seperti pada gambar berikut ini:

Tooltip yang berisi informasi

Tooltip yang berisi informasi

Full table scan akan semakin sia-sia bila saya melakukan pencarian pada tabel yang memiliki banyak record tetapi tidak membutuhkan seluruh baris yang ada. Pada contoh yang saya peroleh, seluruh 443 record yang dicari di tabel produk akan dikembalikan sehingga kerja keras full table scan tidak akan sia-sia.

Operasi LEFT JOIN sudah memakai index. Hal ini terlihat pada dua kotak hijau bertuliskan Non-Unique Key Lookup. Ini adalah foreign key index yang dihasilkan secara otomatis oleh Hibernate (melalui klausa FOREIGN KEY pada saat CREATE TABLE).

Berikutnya, saya menemukan bahwa SELECT DISTINCT pada dasarnya adalah sebuah operasi yang tidak jauh berbeda dengan GROUP BY. Karena SQL ini melibatkan operasi LEFT JOIN, maka MySQL Server tidak bisa begitu saja memakai index. Untuk itu, ia terpaksa harus membuat internal temporary table di memori. Hal ini terlihat dari tulisan tmp_table di bawah kotak DISTINCT.

Internal temporary table adalah tabel di memori yang berisi hasil proses sementara sebelum hasil akhir diperoleh. Saya bisa melihat berapa banyak jumlah internal temporary table yang sudah dibuat oleh MySQL Server (sejak ia dinyalakan) dengan memberikan perintah SQL berikut ini:

SHOW GLOBAL STATUS LIKE 'Created_tmp_tables';

Bila seandainya ukuran internal temporary table mencapai batas tertentu, maka ia akan disimpan ke dalam harddisk dalam bentuk tabel MyISAM. Tentu saja akibatnya adalah query akan menjadi lebih lambat. Untuk melihat jumlah temporay table di memori yang akhirnya disimpan ke harddisk, saya dapat memberikan perintah SQL berikut ini:

SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';

Kapan MySQL Server menyimpan temporary table ke disk? Hal ini tergantung pada nilai tmp_table_size dan max_heap_table_size. Pada sistem dengan jumlah memori yang berlimpah, meningkatkan kedua variabel tersebut akan mencegah MySQL Server untuk menyimpan temporary table di disk sehingga bisa meningkatkan kinerja bila terdapat banyak query yang mengandung DISTINCT dan GROUP BY.

Pada bagian ORDER, saya menemukan tulisan filesort. Ini adalah algoritma yang akan dipakai oleh MySQL Server bila pengurutan tidak dapat dilakukan melalui index. Seperti yang bisa ditebak, filesort akan lebih lambat dibandingkan dengan pengurutan yang dilakukan dengan menggunakan index.

Lalu apa yang harus saya lakukan untuk mengoptimalkan query ini? Cara yang paling realistis adalah dengan melakukan perubahan pada sisi aplikasi, bukan pada sisi database. Apakah pengguna perlu melihat seluruh produk yang ada setiap kali menampilkan screen produk? Pada banyak kasus, jawabannya adalah ‘tidak’.

Seandainya saya menambahkan kriteria pencarian berdasarkan nama pada query di atas, maka saya akan memperoleh hasil seperti berikut ini:

EXPLAIN SELECT ... 
WHERE produk0_.nama = 'namaprodukdicari' 
ORDER BY produk0_.nama ASC;
Hasil EXPLAIN setelah menambahkan kondisi WHERE

Hasil EXPLAIN setelah menambahkan kondisi WHERE

Terlihat bahwa full table scan masih dilakukan! Hanya saja kini tidak dibutuhkan lagi filesort pada ORDER. Mengapa demikian? Hal ini terjadi karena saya tidak memiliki index untuk kolom nama di tabel produk. Memberikan index pada kolom nama untuk produk adalah sesuatu yang lumrah karena biasanya tabel produk lebih sering dibaca daripada ditulis. Oleh sebab itu, saya segera menambahkan index dengan memberikan perintah berikut ini:

ALTER TABLE produk ADD INDEX idx_nama(nama ASC);

Sekarang, hasil visualisasi EXPLAIN untuk query sebelumnya akan terlihat seperti:

Hasil visualisasi EXPLAIN setelah penambahan index pada kolom nama

Hasil visualisasi EXPLAIN setelah penambahan index pada kolom nama

Ini adalah hasil yang paling optimal karena semuanya berwarna ‘hijau’ 🙂

Walaupun sempurna dari sisi kinerja, mucul permasalahan baru yang berkaitan dengan produktifitas. Kondisi pencarian yang saya gunakan pada WHERE adalah pencarian sama dengan seperti WHERE produk0_.nama = 'namaprodukdicari'. Ini berarti pengguna harus memasukkan nama produk secara lengkap dan sama persis seperti yang tersimpan di database! Program yang lebih user-friendly seharusnya memungkinkan pencarian berdasarkan bagian dari nama. Oleh sebab itu, saya perlu mengubah kriteria pada query menjadi WHERE produk0_.nama LIKE '%bagian_nama_produk%' seperti:

EXPLAIN SELECT ... 
WHERE produk0_.nama LIKE '%bagian_nama_produk%' 
ORDER BY produk0_.nama ASC;

Hasil EXPLAIN kini tiba-tiba menjadi tidak se-‘hijau’ sebelumnya! Full table scan kembali terjadi lagi. Mengapa demikian? Kriteria seperti LIKE '%abc%' tidak dapat memanfaatkan index biasa dengan baik karena abc bisa ada dimana saja di nama produk.

Sebagai gantinya, saya bisa melakukan pengorbanan dengan menghilangkan tanda persen (%) di awal. Dengan demikian, query akan mencari nama produk yang diawali dengan apa yang diketik oleh pengguna:

EXPLAIN SELECT ... 
WHERE produk0_.nama LIKE 'namadepan_produk%' 
ORDER BY produk0_.nama ASC;

Hasil visualisasi akan terlihat seperti pada gambar berikut ini:

Index yang membantu kondisi LIKE

Index yang membantu kondisi LIKE

Index Range Scan menunjukkan bahwa index dipakai untuk membantu mencari nama produk yang diawali oleh kriteria pencarian. Ini jauh lebih baik daripada full table scan. Bila seandainya saya tetap ingin mengoptimalkan kriteria pencarian seperti nama LIKE '%abc%', maka saya perlu menggunakan index khusus yang disebut sebagai Full-Text Index. Saya juga perlu mengganti kondisi LIKE menjadi seperti MATCH(nama) AGAINST('abc'). Fasilitas yang disebut sebagai Full-Text Searching (FTS) ini tersedia di InnoDB sejak versi 5.6.4.

Belajar Menyetel Database MySQL Server

Akhir tahun adalah saat yang paling tepat untuk beres-beres setelah setahun menulis kode program. Salah satu contoh bentuk upaya beres-beres tersebut misalnya adalah memeriksa apakah kinerja database yang dipakai oleh aplikasi sudah maksimal. Sama seperti kendaraan bermotor, database juga perlu dirawat secara berkala. Bila yang merawat sepeda motor adalah mekanik di bengkel, maka profesi yang bertugas merawat database dinamakan sebagai database administrator.

Untuk mengukur kinerja program, saya akan mencoba mensimulasikan operasi pengisian sejumlah data penjualan. Operasi ini tidak hanya terdiri atas operasi INSERT, tetapi juga operasi SELECT untuk mencari nama konsumen dan nama produk. Hasil yang saya peroleh adalah dibutuhkan waktu 30.053 ms untuk menyelesaikan query yang ada. Ini adalah perkiraan kasar karena masih belum memperhitungkan jeda waktu yang ditimbulkan oleh jaringan dan kesibukan server saat diakses secara bersamaan. Walaupun demikian, tanpa memperhitungkan hal lain tersebut, apakah nilai ‘dasar’ ini bisa ditingkatkan?

Konfiguras MySQL Server bisa dilakukan dengan mengubah file my.ini. Saya dapat menemukan file ini di lokasi C:\Program Files\MySQL\MySQL Server 5.6 atau di C:\ProgramData\MySQL\MySQL Server 5.6. Untuk lokasi yang lebih akurat, saya dapat menjalankan MySQL dari Command Prompt dan memberikan perintah mysqld --help --verbose | more. Disini akan ada daftar lokasi pada bagian Default options are read from the following files in the given order:. Saya juga dapat melakukan pengaturan secara sementara tanpa mengubah file my.ini dengan memberikan perintah SET GLOBAL. Akan tetapi, karena ingin melakukan perbandingan, saya memilih mengubah file my.ini dan me-restart database setiap kali pengujian dilakukan.

Salah satu pengaturan yang paling klasik adalah mengubah nilai innodb_buffer_pool_size. Ini adalah besarnya wilayah di memori yang dialokasikan khusus untuk menampung cache yang berisi informasi tabel dan index. Semakin besar ukuran buffer pool, maka semakin sedikit operasi disk yang dibutuhkan. Nilai default-nya yang berupa 128 MB merupakan sesuatu yang cukup kecil bila dipakai pada server dengan jumlah memori mencapai gigabyte. Dokumentasi MySQL Server merekomendasikan nilai 80% dari jumlah memori untuk database yang sibuk. Jangan lupa bahwa nilai innodb_buffer_pool_size yang terlalu besar malah bisa mengakibatkan perlambatan akibat paging.

Sebagai percobaan, saya mengubah konfigurasi menjadi seperti berikut ini:

innodb_buffer_pool_size=512M
innodb_log_file_size=512M

Saya juga meningkatkan ukuran innodb_log_file_size untuk mengurangi aktifitas flush. Hasil akhir percobaan setelah perubahan konfigurasi menunjukkan bahwa terdapat peningkatan waktu eksekusi sebesar 7%. Ini adalah peningkatan yang cukup besar. Sayangnya, saya tidak bisa menaikkan innodb_buffer_pool_size menjadi 1GB karena walaupun memiliki free memori lebih dari jumlah tersebut, Windows mensyaratkan lokasi memori bebas harus berlanjut dan tidak boleh tersebar! Batasan ini hanya terasa pada Windows 32-bit dan hampir tidak terjadi di Windows 64-bit (akibat ruang alokasi memori yang masih luas 🙂 ).

Sejak versi 5.6.8, nilai query_cache_type secara default adalah 0 sehingga query cache tidak aktif. Query cache adalah fitur untuk menampung hasil query secara sementara di memori sehingga hasil yang sama bisa dikembalikan dengan cepat untuk SQL yang sama. Tentu saja query cache harus dihapus bila isi tabel yang dibaca sudah berubah sejak terakhir kali dibaca. Bila query cache terlalu sering kadaluarsa, ini bukannya mempercepat malah bisa memperlambat. Dengan demikian, query cache bisa bermanfaat dan juga bisa merugikan tergantung dari seberapa sering aplikasi mengubah isi tabelnya.

Sebagai contoh, saya akan menggunakan mysqlslap bawaan MySQL Server untuk mensimulasikan query SELECT yang dilakukan oleh beberapa klien secara bersamaan dengan memberikan perintah berikut ini:

C:\> mysqlslap --user=root --password --create-schema=inventory 
--concurrency=20 --iterations=10 --no-drop --query=select.sql

Benchmark
        Average number of seconds to run all queries: 25.033 seconds
        Minimum number of seconds to run all queries: 22.435 seconds
        Maximum number of seconds to run all queries: 32.380 seconds
        Number of clients running queries: 20
        Average number of queries per client: 7513

Perintah di atas akan mensimulasikan akses dari 20 client secara bersamaan yang mengerjakan isi query di file select.sql selama 10 kali.

Sebagai perbandingan, saya kemudian mengaktifkan query cache dengan menambahkan baris berikut ini di my.ini:

query_cache_type=1
query_cache_size=5MB

Kali ini, bila saya memberikan perintah yang sama, saya akan memperoleh hasil seperti berikut ini:

C:\> mysqlslap --user=root --password --create-schema=inventory
--concurrency=20 --iterations=10 --no-drop --query=select.sql

Benchmark
        Average number of seconds to run all queries: 36.043 seconds
        Minimum number of seconds to run all queries: 31.943 seconds
        Maximum number of seconds to run all queries: 41.467 seconds
        Number of clients running queries: 20
        Average number of queries per client: 7513

Terlihat bahwa mengaktifkan query cache hanya membuat query menjadi lambat. Mengapa bisa demikian? Untuk melihat status query cache, saya dapat memberikan perintah SQL berikut ini:

mysql> SHOW STATUS LIKE 'Qcache%';
+-------------------------+---------+
| Variable_name           | Value   |
+-------------------------+---------+
| Qcache_free_blocks      | 327     |
| Qcache_free_memory      | 988680  |
| Qcache_hits             | 1196351 |
| Qcache_inserts          | 65103   |
| Qcache_lowmem_prunes    | 64153   |
| Qcache_not_cached       | 240682  |
| Qcache_queries_in_cache | 950     |
| Qcache_total_blocks     | 2351    |
+-------------------------+---------+

Nilai Qcache_lowmem_prunes menunjukkan jumlah query yang terpaksa dihapus karena ukuran query cache sudah tidak cukup lagi. Pada hasil di atas, nilainya cukup tinggi. Walaupun demikian, nilai Qcache_hits yang tinggi menunjukkan bahwa query cache seharusnya bisa membantu. Oleh sebab itu, saya akan mencoba meningkatkan ukuran query cache dengan mengubah konfigurasi menjadi seperti berikut ini:

query_cache_type=1
query_cache_size=10MB

Hasil ketika menjalankan mysqlslap adalah:

C:\> mysqlslap --user=root --password --create-schema=inventory
--concurrency=20 --iterations=10 --no-drop --query=select.sql

Benchmark
        Average number of seconds to run all queries: 24.233 seconds
        Minimum number of seconds to run all queries: 22.326 seconds
        Maximum number of seconds to run all queries: 31.803 seconds
        Number of clients running queries: 20
        Average number of queries per client: 7513

Walaupun sudah tidak lambat lagi, saya tidak menjumpai peningkatkan kinerja yang cukup berarti saat mengaktifkan query cache. Hal ini cukup masuk akal karena saat ini belum ada tabel raksasa yang membutuhkan waktu pencarian lama sehingga query cache belum dapat menunjukkan taringnya.

Bicara soal jaringan, MySQL Server memiliki kebiasaan melakukan request DNS untuk melakukan validasi host pada saat pengguna login. Bila DNS server yang dipakai lambat (misalnya bawaan ISP), maka hal ini bisa menimbulkan kesan bahwa koneksi database sangat lambat pada aplikasi pertama kali dijalankan. Untuk mengatasinya, saya bisa mematikan request DNS dan hanya melakukan validasi berdasarkan IP dengan menambahkan konfigurasi berikut ini:

skip_name_resolve=ON

Fitur lainnya yang berguna untuk tabel berukuran besar adalah kompresi tabel. MySQL Server mendukung 2 jenis file format untuk InnoDB: Antelope (versi original) dan Barracuda (versi terbaru). File format yang dipakai secara default adalah Antelope untuk memaksimalkan kompatibilitas dengan fitur lama. Karena suka ber-eksperimen, saya akan beralih ke Barracuda dengan menambahkan baris berikut ini di file konfigurasi:

innodb_file_format=Barracuda

Salah satu fitur baru yang ditawarkan oleh format Barracuda adalah kompresi tabel. Kompresi dapat diakses dengan menambahkan ROW_FORMAT=COMPRESSED pada saat memberikan perintah CREATE TABLE atau ALTER TABLE seperti pada:

ALTER TABLE `produk`
ROW_FORMAT = COMPRESSED ;

Untuk bisa memakai kompresi, saya perlu memastikan bahwa nilai innodb_file_per_table adalah ON (nilai default sejak versi 5.6.6). Lagi-lagi saya tidak memiliki tabel yang berukuran sangat besar sehingga saya tidak menjumpai peningkatan kinerja yang cukup berarti dengan mengaktifkan kompresi tabel.

Melakukan Profiling Dengan Zend Studio

Profiling adalah sebuah aktifitas untuk mengukur kode program, seperti waktu eksekusi sebuah function atau jumlah pemanggilan sebuah function. Dengan melakukan profiling, seorang programmer akan mengetahui bagian mana dari kode programnya yang lambat atau bagian mana yang lebih sering dipanggil. Zend Studio dilengkapi dengan profiler untuk situs PHP. Syarat untuk menggunakan profiler tersebut adalah server yang men-host aplikasi PHP harus memiliki extension Zend Debugger. Karena saya memakai Zend Server CE (versi gratis), maka Zend Debugger sudah ter-install dan aktif.

Untuk melakukan profiling dari Zend Studio, saya dapat men-klik kanan sebuah halaman PHP, lalu memilih Profile As, PHP Web Application. Sebagai alternatif lainnya, saya dapat memilih menu Run, Profile URL dan memasukkan URL yang merujuk ke halaman PHP yang akan diperiksa. Selain itu, bila saya men-install Zend Studio Toolbar di browser, maka saya dapat langsung men-klik tombol Profile saat mengunjungi sebuah halaman PHP seperti yang terlihat pada gambar berikut ini:

Melakukan profiling dari browser

Melakukan profiling dari browser

Setelah itu, Zend Studio akan menampilkan konfirmasi untuk beralih ke PHH Profile perspective. Bagi yang belum terbiasa dengan Eclipse, sebuah perspective adalah kumpulan beberapa window dengan lokasi tertentu yang biasanya dipakai untuk keperluan tertentu. Perspective yang biasa dipakai untuk mengembangkan kode program adalah PHP perspective. Untuk melakukan debugging, terdapat Debug perspective. Pengguna dapat berganti perspective dengan men-klik icon perspective yang diinginkan di sisi kanan atas Eclipse, atau memilih menu Window, Open Perspective. Bagi yang terbiasa dengan produk Adobe, perspective di Eclipse mirip seperti workspace di Adobe. Sebagai contoh, pada Adobe Photoshop CS5, saya dapat memilih workspace seperti Essentials, Design, Painting, 3D dan sebagainya. Jadi, jangan lupa bila ingin kembali coding, ganti perspective dari PHP Profile persepective menjadi PHP perspective.

Pada Profile Information, saya dapat menemukan grafis pie yang menunjukkan waktu eksekusi kode program, seperti yang terlihat pada gambar berikut ini:

Diagram pie untuk waktu ekseksusi

Diagram pie untuk waktu ekseksusi

Kode program yang saya profile adalah sebuah halaman PHP sederhana yang mengambil data dari database dan menampilkannya pada view dengan menggunakan Twig. Grafis di atas memperlihatkan bahwa Autoloader.php (untuk loading class yang dibutuhkan Twig) dan Database.php (melakukan query database) membutuhkan waktu paling lama.

Saya dapat menemukan informasi lebih detail di Execution Statistics seperti yang terlihat pada gambar berikut ini:

Tabel untuk execution statistics

Tabel untuk execution statistics

Sebagai contoh, gambar di atas memperlihatkan bahwa function autoload() di Autoloader.php dikerjakan sebanyak 70 kali. Saya dapat melihat informasi lebih detail lagi dengan men-klik kanan function tersebut dan memilih Open function invocation statistics. Zend Studio profiler akan menampilkan informasi seperti yang terlihat pada gambar berikut ini:

Tabel untuk invocation statistics

Tabel untuk invocation statistics

Bagian Selected function is invoked by: pada tab Function Invocation Statistics akan memperlihatkan siapa saja yang mengerjakan function autoload(). Sebagai informasi, ini adalah fungsi yang akan dikerjakan setiap kali terdapat penggunaan sebuah class baru yang belum dikenali. autoload() bertugas men-include file yang mewakili deklarasi class tersebut. Saya dapat memperoleh informasi lebih lanjut class apa saja yang dibuat dengan memeriksa bagian Selected function invokes: seperti yang terlihat pada gambar berikut ini:

Tabel untuk invoke statistics

Tabel untuk invoke statistics

Pada tab Execution Flow, saya dapat melihat statistik function dimana setiap function diurutkan berdasarkan urutan eksekusi, seperti yang terlihat pada gambar berikut ini:

Tabel untuk execution flow

Tabel untuk execution flow

Apa itu Zend Optimizer+ Di Zend Server?

Sebuah fakta yang tidak dapat dipungkiri adalah PHP kalah dari Java dalam hal kinerja bila dilihat dari sisi arsitektur kedua teknologi tersebut. Tapi beberapa mahasiswa yang baru belajar tampaknya sulit menerima fakta ini. Beberapa dari mereka yang kritis akan bertanya seperti “Startup server Java lebih lama dibanding PHP, kenapa Java bisa lebih kencang?” atau “Bukankah Java butuh lebih banyak memori dibanding PHP?” Apa yang mereka ungkapkan memang benar, tapi saat request dari pengguna web diproses, server berbasis Java memiliki peluang besar untuk menghasilkan respon lebih cepat. Mengapa demikian?

Alasan pertama, kode program Java di-compile terlebih dahulu menjadi bytecode (file class) oleh programmer-nya. Apa yang di-deploy ke server Java adalah hasil kompilasi (file class) dalam bentuk file jar, bukan kode program Java! Dengan demikian, server dapat langsung menjalankannya. Lalu bagaimana dengan PHP? Tidak ada compiler PHP yang perlu dipanggil oleh programmer PHP!! Programmer PHP langsung men-deploy aplikasi web dalam bentuk kode program PHP (file PHP). Oleh sebab itu, saat sebuah halaman PHP dibuka, maka kode program di file PHP tersebut perlu di-parse dan di-compile ke dalam bytecode sebelum bisa di-eksekusi oleh CPU.

Alasan kedua, server Java adalah sebuah container dimana seluruh objek yang masih berguna tetap ‘hidup’ dan dipakai lagi oleh objek lainnya. Berbeda dengan Java, file PHP akan kembali dikerjakan pada saat request diberikan dan setelah selesai dikerjakan, semuanya ‘lenyap’. Sebagai contoh, perhatikan kode program berikut ini:

<?php
class Counter {             
    public static $counter;     
}

print "Counter: " . ++Counter::$counter;    
?>

Keyword static menunjukkan bahwa property counter dapat diakses kapan saja selama class Counter ada. Bila saya menjalankan kode program PHP di atas berkali-kali, nilai $counter akan selalu 1. Hal ini karena setiap kali saya membuka halaman PHP tersebut, class Counter baru akan tercipta dan setelah mencapai akhir eksekusi, class Counter tersebut akan musnah. Satu-satunya cara untuk mempertahankan nilai adalah memakai session, teknologi caching seperti Memcache, atau menyimpan nilai ke database. Perilaku ini berbeda dengan Java: class Counter akan selalu ada di memori dan nilai counter tetap dapat diakses setiap saat (termasuk dari request berbeda). Sebagai konsekuensinya, server Java membutuhkan lebih banyak memori guna menampung object yang masih ‘hidup’.

Alasan ketiga, JVM yang dipakai untuk server Java biasanya memiliki fasilitas Just In Time (JIT) code generation. Saat bytecode Java di-eksekusi pertama kali, JIT akan melakukan analisa dan bila perlu akan mengubah bytecode tersebut menjadi instruksi mesin (native) yang dapat langsung dikerjakan oleh CPU. Hal ini menyebabkan lambatnya eksekusi bytecode untuk pertama kali, tetapi eksekusi selanjutnya menjadi jauh lebih cepat (setara dengan bahasa native di platform tersebut). PHP tidak memiliki fasilitas seperti ini. Untuk itu, Facebook mengembangkan HipHop Virtual Machine (HHVM) yang merupakan sebuah execution engine untuk PHP. HHVM adalah proyek open source yang dapat dilihat di https://github.com/facebook/hhvm dan telah dipakai oleh situs facebook.com sejak awal tahun 2013. HHVM memiliki JIT code generation. Facebook telah menciptakan ‘PHP virtual machine’ yang dapat dibandingkan dengan Java Virtual Machine (JVM)! Tapi ini adalah contoh kasus tidak lazim. Kebanyakan fans awam menggunakan PHP ‘biasa’ (Zend Engine) seperti pada konfigurasi XAMPP 😉

Jadi, PHP akan selalu men-parse dan men-compile file PHP setiap kali ia dipanggil. Bila file PHP tersebut men-include file lainnya, maka file lain tersebut juga perlu di-parse dan di-compile. Bila saya memakai framework (seperti Zend Framework dan Symphony), kemungkinan akan ada banyak file yang harus di-include. Ini belum lagi ditambah fakta bahwa proses parsing dan kompilasi ke bytecode akan dikerjakan kembali setiap kali halaman diakses pengguna!!

Salah satu cara untuk menghindari hal tersebut adalah dengan memakai teknologi caching (sering juga disebut sebagai PHP accelerator) seperti Alternative PHP Cache (APC) dan Zend Optimizer+. Karena saya menggunakan Zend Server 6.2.0, saya sudah memperoleh fasilitas Zend Optimizer+. Sebagai informasi, Zend Optimizer+ sudah terintegrasi pada PHP 5.5 sehingga tidak perlu di-install secara terpisah lagi. Ia mengalami perubahan nama menjadi OPcache dimana informasi lebih lanjut dapat ditemukan di http://www.php.net/manual/en/intro.opcache.php.

Zend Optimizer+ akan menampung hasil kompilasi file PHP dalam bentuk bytecode di memori. Bila selanjutnya ada request ke file PHP tersebut, Zend Optimizer+ akan langsung mengerjakan bytecode yang sudah ada di memori tanpa harus melalui proses parsing dan kompilasi lagi.

Untuk memeriksa apakah Zend Optimizer+ sudah diaktifkan, saya akan masuk ke dashboard Zend Server dengan membuka URL http://localhost:10081. Setelah memilih menu Configurations, Components, saya akan menemukan status loaded di Zend Optimizer+ seperti yang terlihat pada gambar berikut ini:

Melihat status Zend Optimizer+ di Zend Server

Melihat status Zend Optimizer+ di Zend Server

Pada halaman ini, saya juga dapat melakukan pengaturan lebih lanjut untuk Zend Optimizer+ dengan men-klik panan bawah di ujung kanan komponen tersebut, seperti yang terlihat pada gambar berikut ini:

Pengaturan Zend Optimizer+ di Zend Server

Pengaturan Zend Optimizer+ di Zend Server

Zend Optimizer+ akan bekerja secara transparan tanpa campur tangan pengguna. Namun, ia menyediakan API accelerator_reset() dan accelerator_get_status() yang dapat dipakai di kode program untuk melakukan reset dan memantau statistik. Sebagai contoh, berikut ini adalah kode program PHP yang menampilkan statistik untuk Zend Optimizer+:

<html>
<body>
<?php
    $stats = accelerator_get_status();

    print "<h3>Penggunaan Memori Untuk Zend Optimizer+</h3>";
    $memoriDipakai = round($stats['memory_usage']['used_memory'] / 1024 / 1024);
    $memoriBebas = round($stats['memory_usage']['free_memory'] / 1024 / 1024);
    $memoriWasted = round($stats['memory_usage']['wasted_memory'] / 1024 / 1024);
    $persenWasted = round($stats['memory_usage']['current_wasted_percentage'], 2);
    print "<p>Memori yang dipakai = $memoriDipakai MB</p>";
    print "<p>Memori yang invalid = $memoriWasted MB ($persenWasted %)</p>";
    print "<p>Memori bebas = $memoriBebas MB</p>";

    print "<h3>Statistik</h3>";
    print "<p>Jumlah script yang di-cache: {$stats['accelerator_statistics']['num_cached_scripts']}</p>";
    print "<p>Jumlah script yang dapat ditampung: {$stats['accelerator_statistics']['max_cached_scripts']}</p>";
    print "<p>Jumlah hit (cache yang dipakai): {$stats['accelerator_statistics']['hits']}</p>";
    print "<p>Jumlah miss (tidak ada dalam cache): {$stats['accelerator_statistics']['misses']}</p>";
    $hitRate = round($stats['accelerator_statistics']['accelerator_hit_rate'], 2);
    print "<p>Hit rate: $hitRate %</p>";
?>

</body>
</html>

Saat menjalankan file PHP di atas, saya akan memperoleh output seperti:

Penggunaan Memori Untuk Zend Optimizer+

Memori yang dipakai = 14 MB

Memori yang invalid = 0 MB (0 %)

Memori bebas = 50 MB

Statistik

Jumlah script yang di-cache: 622

Jumlah script yang dapat ditampung: 3907

Jumlah hit (cache yang dipakai): 18440

Jumlah miss (tidak ada dalam cache): 628

Hit rate: 96.71 %

Pada konfigurasi default, Zend Optimizer+ akan menggunakan memori sebesar 64 MB untuk menampung bytecode hasil kompilasi. Saya dapat mengubah nilai zend_optimizerplus.memory_consumption untuk meningkatkan jumlah memori yang dipakai bila perlu. Nilai hit rate menunjukkan indikator kinerja Zend Optimizer+, dimana nilai yang tinggi merupakan indikator kinerja yang baik.

Saya akan mencoba melihat peningkatkan kinerja yang diberikan oleh Zend Optimizer+ dengan mensimulasikan request dari 100 pengguna dimana masing-masing pengguna memberikan 10 request ke sebuah controller yang akan menampilkan sebuah view dengan memakai Twig. Seperti biasa, saya akan menggunakan Apache JMeter untuk mengukur waktu respon dari Zend Server. Tanpa Zend Optimizer+, saya memperoleh hasil seperti pada gambar berikut ini ini:

Hasil grafis tanpa Zend Optimizer+

Hasil grafis tanpa Zend Optimizer+

Hasil summary report terlihat seperti pada gambar berikut ini:

Hasil summary tanpa Zend Optimizer+

Hasil summary tanpa Zend Optimizer+

Kemudian, saya menjalankan ulang pengujian yang sama, tetapi kali ini dengan mengaktifkan Zend Optimizer+. Hasil grafis dari JMeter yang saya peroleh adalah:

Hasil grafis dengan Zend Optimizer+

Hasil grafis dengan Zend Optimizer+

Hasil summary report kali ini terlihat seperti pada gambar berikut ini:

Hasil summary dengan Zend Optimizer+

Hasil summary dengan Zend Optimizer+

Bila saya membandingkan nilai throughput kedua hasil di atas, saya akan menemukan selisih yang cukup jauh. Mengaktifkan Zend Optimizer+ meningkatkan throughput dari 11,3/sec menjadi 41,6/sec. Peningkatkan yang dicapai hampir mendekati 4 kali lipat. Tentunya peningkatan kinerja akan lebih terasa lagi bila terdapat semakin banyak file yang di-include (seperti saat memakai framework atau CMS).

Memeriksa Waktu Eksekusi SQL Di Oracle TimesTen

Pada artikel sebelumnya, saya menuliskan bagaimana memantau kinerja database TimesTen melalui procedure di package TT_STATS.   Pada artikel ini, saya akan mencoba memeriksa kinerja eksekusi masing-masing SQL dengan menggunakan procedure bawaan TimesTen seperti ttStatsConfig, ttSQLCmdCacheInfo dan sebagainya.

Saya akan mulai dengan mengambil sampel untuk setiap eksekusi SQL di TimesTen dengan memberikan perintah berikut ini:

Memulai proses pengambilan sampel

Memulai proses pengambilan sampel

Pada anonymous PL/SQL di atas, saya mengaktifkan pengambilan sampel untuk setiap kali eksekusi SQL dengan memanggil ttStatsConfig("SQLCmdSampleFactor", 1).   Selain itu, saya juga menghapus statistik yang sudah diambil sebelumnya dengan memanggil ttStatsConfig("SQLCmdHistogramReset", 0).   Saya menentukan jenis statistik yang akan diambil dengan memanggil ttStatsConfig("StatsLevel", "TYPICAL").

Setelah mengaktifkan proses pengambil sampel, saya perlu menjalankan aplikasi dan melakukan operasi yang berkaitan dengan database di aplikasi saya.   TimesTen akan mengambil statistik untuk setiap SQL yang dikerjakan oleh aplikasi  secara otomatis.   Setelah selesai, saya mematikan aplikasi dan kembali ke SQL Developer.

Saya dapat menampilkan statistik yang telah terkumpul dengan memberikan perintah seperti berikut ini:

CALL ttSQLCmdCacheInfo;

Untuk melihat informasi khusus untuk SQL tertentu, saya dapat memanggil ttSQLCmdCacheInfo dengan melewatkan parameter berupa id dari perintah SQL tersebut, seperti yang terlihat pada gambar berikut ini:

Melihat statistik untuk SQL tertentu

Melihat statistik untuk SQL tertentu

Saya juga dapat memanggil ttSQLCmdCacheInfo2 yang menyediakan informasi tambahan berupa fetchCount, startTime, maxExecuteTime, lastExecuteTime, dan minExecuteTime.

Untuk melihat histogram yang menunjukkan persebaran waktu eksekusi untuk perintah SQL yang ada, saya memberikan perintah seperti yang terlihat pada gambar berikut ini:

Melihat histogram waktu eksekusi SQL

Melihat histogram waktu eksekusi SQL

Pada hasil di atas, terlihat bahwa terdapat 10.971 eksekusi SQL yang dikerjakan sejak saya memulai proses pengambilan sampel.   Total waktu yang dibutuhkan untuk mengerjakan seluruh SQL tersebut adalah 1,0693366 detik.   Dari seluruh SQL tersebut, terdapat 9.669 eksekusi yang membutuhkan waktu lebih dari 0,00001562 detik hingga 0,000125 detik.   Terdapat 1.207 eksekusi yang membutuhkan waktu lebih dari 0,000125 detik hingga 0,001 detik.   Dan seterusnya.

Berdasarkan hasil histogram, terlihat bahwa kebanyakan eksekusi SQL di aplikasi saya memiliki waktu eksekusi antara 0.00001563 detik hingga 0.000125 detik.   Tidak ada SQL yang waktu eksekusinya mencapai hingga 1 detik.   Ini adalah hasil yang cukup memuaskan mengingat isi tabel yang diproses adalah kumpulan transaksi jual beli selama beberapa tahun.

Memantau Kinerja Oracle TimesTen Dengan Package TT_STATS

Sebuah sepeda motor perlu dirawat secara berkala.   Perawatan yang dikenal sebagai engine tune-up ini berguna untuk menjaga agar kinerja mesin motor tetap optimal dengan cara melakukan kalibrasi ulang karbulator, mengganti spark plug (busi) yang usang, dan sebagainya.   Hampir semua pemilik motor mengerti bahwa sepeda motor yang mereka beli tidak bisa dipakai selamanya tanpa di-tune-up di bengkel.   Begitu juga dengan sebuah database; ia perlu dirawat dan dipantau secara berkala.   Bedanya, tidak seperti pemilik sepeda motor yang menyadari pentingnya tune-up, beberapa programmer pemula menganggap bahwa setelah program selesai dibuat, maka database dapat dibiarkan begitu saja hingga suatu hari nanti menjadi penuh.

Lalu, bagaimana cara memantau kinerja database TimesTen?   Saya dapat menggunakan tool bawaan ttStats atau langsung memanggil procedure yang ada di package TT_STATS.   Pada artikel ini, saya akan mencoba memakai beberapa procedure yang ada di package TT_STATS.

Saya akan mulai dengan memanggil procedure TT_STATS.CAPTURE_SNAPSHOT() seperti yang terlihat pada gambar berikut ini:

Memulai proses capture

Memulai proses capture

Pada pemanggilan TT_STATS.CAPTURE_SNAPSHOT di atas,  saya melewatkan nilai parameter capture_level berupa 'ALL' sehingga seluruh metrik yang ada akan di-capture.   Pilihan lainnya adalah 'NONE', 'BASIC' dan 'TYPICAL'.

Selain itu, saya juga menggunakan TT_STATS.SHOW_SNAPSHOTS untuk menampilkan seluruh snapshot yang tersedia.   Terlihat bahwa id snapshot yang barusan dibuat adalah 8.

Saya kemudian menjalankan aplikasi dan mengerjakan beberapa operasi yang melibatkan database.   Setelah selesai, saya kembali membuat sebuah snapshot baru di TimesTen.   Karena perintah tersebut sudah ada di worksheet SQL Developer, saya hanya perlu men-klik icon Run Script atau menekan tombol F5.

Sekarang, procedure TT_STATS.SHOW_SNAPSHOTS akan menampilkan dua buah snapshot berbeda, yaitu snapshot dengan id 8 dan id 9, seperti yang terlihat pada gambar berikut ini:

Men-capture snapshot kedua

Men-capture snapshot kedua

Output yang saya harapkan adalah laporan yang membandingkan kedua snapshot tersebut.   Saya dapat menggunakan procedure TT_STATS.GENERATE_REPORT_HTML untuk menghasilkan laporan dalam bentuk HTML, atau TT_STATS.GENERATE_REPORT_TEXT untuk menghasilkan laporan dalam bentuk teks biasa.   Sebagai latihan, saya akan menampilkan laporan dalam bentuk teks dengan perintah seperti berikut ini:

Membuat laporan teks

Membuat laporan teks

Pada gambar di atas, saya membuat sebuah anonymous PL/SQL yang memanggil TT_STATS.GENERATE_REPORT_HTML.   Parameter pertama dan parameter kedua adalah id dari snapshot yang hendak dibandingkan.   Parameter ketiga adalah sebuah associative array yang menampung isi laporan yang dihasilkan baris per baris.   Karena nilai associative array tidak dapat ditampilkan secara langsung, saya kemudian menggunakan looping for untuk mencetak isi associative array tersebut.

Sebagai langkah penutup, saya akan menghapus yang telah dihasilkan guna menghemat memori dengan memberikan perintah berikut ini:

Menghapus snapshot

Menghapus snapshot

Procedure TT_STATS.DROP_SNAPSHOTS_RANGE menerima parameter berupa id snapshot pertama hingga id snapshot terakhir yang akan dihapus.   Bila keduanya bernilai 0, maka seluruh snapshot yang ada akan dihapus.

Memakai Oracles TimesTen Dengan simple-jpa

Oracle TimesTen adalah sebuah database in-memory yang mendukung SQL.   Karena TimesTen masih mendukung SQL, maka ia dapat dipakai pada aplikasi yang menggunakan Java Persistence API (JPA) dalam mengakses database.   Oleh sebab itu, saya akan mencoba melakukan migrasi database pada sebuah aplikasi Griffon yang memakai plugin simple-jpa.   Aplikasi desktop tersebut sebelumnya memakai database MySQL, dan saya ingin mengubahnya agar memakai database Oracle TimesTen.   Apa saja perubahan yang harus dilakukan?

Langkah pertama adalah menambahkan driver JDBC ke proyek saya.   Driver JDBC untuk TimesTen tidak ada di repository global Maven (karena ia bukan produk open-source!) sehingga saya tidak bisa menambahkannya pada file BuildConfig.groovy seperti biasa.   Sebagai alternatifnya, saya akan men-copy file C:\TimesTen\tt1122_32\lib\ttjdbc7.jar secara manual ke folder C:\proyekGriffon\lib.

Langkah berikutnya adalah menyiapkan dialek TimesTen untuk Hibernate JPA.   Setiap database, walaupun mendukung SQL secara umum, masing-masing memiliki sedikit perbedaan.   Sebagai contoh, tipe data tulisan (karaketer) yang disarankan untuk TimesTen dan Oracle Database adalah VARCHAR2, sementara pada MySQL yang tersedia adalah VARCHAR.   Contoh lainnya, untuk melihat tanggal hari ini di MySQL, saya memakai function seperti SELECT CURRENT_DATE. Sementara itu, jika di TimesTen dan Oracle Database, saya akan memakai function SELECT SYSDATE FROM DUAL.

Hibernate JPA dapat mendukung berbagai variasi SQL pada masing-masing database, yang disebutnya sebagai dialek.   Secara bawaan, Hibernate memiliki beberapa dialek yang dapat dipilih dengan cara dilewatkan melalui property hibernate.dialect.  Contoh dialek bawaan adalah MySQLMyISAMDialect, MySQLInnoDBDialect, Oracle9Dialect, PostgreSQL81Dialect, SAPDBDialect, dan sebagainya.   Sayang sekali, dari seluruh dialek bawaan Hibernate, tidak ada dialek untuk TimesTen.  Tapi saya tidak perlu khawatir karena TimesTen telah menyediakan dialek untuk dipakai di Hibernate.   Lokasinya terletak di C:\TimesTen\tt1122_32\quickstart\sample_code\j2ee_orm\config\hibernate4\TimesTenDialect1122.java.   Saya segera membuat folder untuk package org.hibernate.dialect dan meletakkan file tersebut pada folder ini, seperti yang diperlihatkan oleh gambar berikut ini:

Menambah dialek TimesTen untuk Hibernate

Menambah dialek TimesTen untuk Hibernate

Ada sedikit kekurang TimesTenDialect1122 yang perlu saya perbaiki terlebih dahulu.   Sama seperti Oracle Database, TimesTen tidak mendukung tipe data boolean di SQL.   Sementara itu, di MySQL, bila saya mendefinisikan sebuah kolom dengan tipe data boolean, ia akan diterjemahkan menjadi tinyint(1).   Oleh sebab itu, saya menambahkan baris berikut ini pada class TimesTenDialect1122:

registerColumnType(Types.BOOLEAN, "TT_TINYINT");

Sekarang, saya akan melakukan perubahan pada file persistence.xml sehingga terlihat seperti berikut ini:

<persistence ...>
  <persistence-unit name="default" transaction-type="RESOURCE_LOCAL">
    ...
    <properties>
      <property name="javax.persistence.jdbc.driver" value="com.timesten.jdbc.TimesTenDriver"/>
      <property name="javax.persistence.jdbc.url" value="jdbc:timesten:direct:LATIHAN"/>
      <property name="javax.persistence.jdbc.user" value="solid"/>
      <property name="javax.persistence.jdbc.password" value="snake"/>
      <property name="hibernate.connection.autocommit" value="false"/>
      <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
      <property name="hibernate.dialect" value="org.hibernate.dialect.TimesTenDialect1122"/>
      <property name="jadira.usertype.autoRegisterUserTypes" value="true"/>
    </properties>
  </persistence-unit>
</persistence>

Saya mengubah nilai property hibernate.hbm2dll.auto menjadi create-drop supaya Hibernate membuat tabel baru di database TimesTen yang masih kosong.   Baris ini boleh dihapus bila tidak dibutuhkan lagi.

Lalu, apa langkah berikutnya? Cukup sampai disini!   Saya tidak perlu mengubah kode program karena saya tidak memberikan perintah SQL secara langsung, melainkan melalui melalui dynamic finders dan JP QL (bahasa query untuk JPA).   Dengan demikian, saya tidak perlu khawatir dengan SQL yang tidak compatible antara MySQL dan TimesTen (selama dialek Hibernate-nya tidak bermasalah!).

Tapi ada satu pandangan naif yang perlu dihindari, terutama bagi programmer yang tidak mau berurusan dengan cara kerja database, yaitu menganggap bahwa dengan memakai sebuah database ‘keren’ maka kinerja aplikasi secara otomatis akan meningkat.   Ini adalah sebuah pandangan yang salah karena masing-masing database memiliki karakteristik tersendiri.   Terkadang aplikasi harus menyesuaikan dengan karakteristik database.   Sebagai contoh, ada dua ‘aliran‘ dalam mengakses data di memori.   Sebuah tabel terdiri atas dua dimensi (baris dan kolom) dengan data seperti:

        Kolom1   Kolom2   Kolom3
---------------------------------
Baris1   A1      B1       C1
Baris2   A2      B2       C2
Baris3   A3      B3       C3   
---------------------------------

Skema di atas adalah visualisasi tabel dalam benak manusia. Tapi saat disimpan di memori dan piringan harddisk, segala sesuatunya disimpan dalam blok per blok (anggap setiap blok adalah sebuah kotak yang dapat diisi).  Setiap blok di memori (atau blok di sektor harddisk) memiliki alamat berupa posisi offset, misalnya dari  0 hingga ke byte terakhir di offset 0xffffffff.   Jadi, di media penyimpanan, struktur tabel yang dua dimensi (baris dan kolom) harus disimpan ke dalam struktur satu dimensi (kumpulan blok).  Perancang database (pembuat DBMS) dapat memilih menyimpan data tabel per baris sehingga data akan terlihat seperti: A1 B1 C1 A2 B2 C2 A3 B3 C3.   Tapi ia juga dapat memilih menyimpan per kolom sehingga data di memori akan terlihat seperti: A1 A2 A3 B1 B2 B3 C1 C2 C3.   Tabel yang disimpan per kolom memungkinkan query yang cepat (seperti agregasi per kolom) dan memungkinkan kompresi kolom.   Tapi kelemahannya adalah proses penambahan data baru menjadi lebih lambat.   Hal ini menyebabkan DBMS yang menyimpan data per baris lebih tepat dipakai untuk aplikasi OLTP (pemrosesan transaksi), sementara DBMS yang menyimpan data per kolom lebih tepat dipakai untuk aplikasi OLAP (analisa dan laporan).  Contoh sederhana ini menunjukkan bahwa sebaiknya aplikasi mengerti karakteristik database sehingga dapat menggunakannya secara maksimal.

Kembali ke aplikasi saya, saat setelah mengganti database dari MySQL ke TimesTen, saya mencoba menguji perbedaan kecepatannya.   Saya membuat kode program yang menambah 94.521 faktur (termasuk diantaranya query untuk mencari konsumen berdasarkan kode, mencari barang berdasarkan kode, dan menambah pembayaran).   Saat memakai database MySQL, transaksi tersebut selesai dalam waktu 13 menit. Sementara bila memakai TimesTen, dibutuhkan waktu hingga mencapai 14 menit!   Mengapa TimesTen lebih lambat dibanding MySQL??

Investigasi lebih lanjut menunjukkan bahwa TimesTen kesulitan menangani transaksi yang isinya sangat besar.   Pada percobaan pertama, 94.521 faktur tersebut merupakan bagian dari sebuah transaksi tunggal.   Untuk percobaan kedua, saya menjadikan setiap proses penyimpanan masing-masing faktur menjadi sebuah transaksi tunggal.   Hasilnya? Oracle TimesTen berhasil memproses seluruh faktur dalam waktu 10 menit!   MySQL malah kewalahan melakukan banyak commit sehingga pada percobaan kedua ini, ia membutuhkan waktu hingga 49 menit.   Kali ini, TimesTen hampir lima kali lebih cepat dibandingkan dengan MySQL.

Melihat Kinerja Aplikasi Desktop Berbasis Griffon dan simple-jpa

Sepuluh tahun silam saat bekerja sebagai developer di salah satu software house, seorang technical leader melarang saya untuk memakai Java Reflection dalam menghasilkan data transfer objects (DTO).   Alasannya: reflection lebih ‘berat’ dibanding copy paste!   Karena larangan tersebut, saya dan rekan-rekan menghabiskan waktu hampir seminggu lebih untuk modul tersebut.   Delivery proyek menjadi terlambat.   Iya, pada zaman tersebut, memakai reflection memang lebih ‘berat’ , tapi pertanyaan penting yang sering terlupakan adalah: seberapa ‘berat‘ atau seberapa lambat??   Apakah penalti kinerja ini cukup berharga untuk ditukarkan dengan berkurangnya ketepatan waktu dalam delivery proyek?  Apakah penalti kinerja ini memiliki poin lebih penting sehingga tidak apa-apa bila aplikasi menjadi rumit dan sulit di-maintain? Mempertimbangkan kinerja di atas segala-galanya adalah salah satu kecendurangan negatif dalam dunia software engineering yang disebut sebagai premature optimization.

Griffon adalah sebuah framework untuk aplikasi desktop yang berdasarkan pada bahasa Groovy.   Groovy sendiri adalah sebuah bahasa pemograman dinamis yang lebih lambat dibandingkan bahasa statis seperti Java.   Tapi pertanyaan adalah: seberapa lambat sebuah aplikasi desktop yang dikembangkan dengan Groovy? Apakah masih bisa diterima dalam batas kewajaran?

Untuk menjawab pertanyaan tersebut, saya akan melakukan pengujian dengan menggunakan sebuah aplikasi desktop yang dibuat dengan framework Griffon dan plugin simple-jpa.   Aplikasi ini akan membaca data invoice (faktur) dari sebuah file teks kemudian memproses setiap baris data tersebut: menyimpan invoice, menambah entri pada inventory, serta menambah entri pada pembayaran bila diperlukan.   Data lainnya yang dibutuhkan oleh invoice sudah tersimpan di tabel dengan jumlah yang mendekati pemakaian sehari-hari. Sebagai contoh, di tabel produk terdapat 7.360 record.  Sebuah jumlah yang tidak terlalu kecil dan tidak terlalu besar.

Jumlah baris yang harus diproses oleh aplikasi ini adalah 91.190 baris yang mewakili data invoice dan item-nya.   Aplikasi ini akan memakai Griffon 1.2.0, simple-jpa 0.4.2, Hibernate JPA 4.2.0.Final dan MySQL Server 5.6.11.   Selama pengujian, database hanya akan dipakai oleh program tersebut saja.   Pengujian dipakai untuk mensimulasikan aktifitas single user.  Btw, hasil percobaan ini hanya sebagai gambaran umum yang terbatas pada aplikasi yang saya uji tersebut.   Untuk hasil yang lebih akurat tentunya dibutuhkan aplikasi dan environment yang lebih terkendali dan fair.

Percobaan 1

Saya akan mulai dengan menjalankan aplikasi dalam modus hanya membaca dari database.   Pengguna biasanya akan lebih sering melihat data inventory atau mencari data pembayaran ketimbang membuat data invoice baru.   Hasil percobaan menunjukkan bahwa 91.190 baris data berhasil diproses dalam waktu 49 menit.   Ini sudah meliputi membaca tabel produk, membaca data suplier/konsumen, mengerjakan business logic, tapi hasil prosesnya tidak disimpan (tidak ada query INSERT).

Aplikasi percobaan berbasis Griffon dan simple-jpa ini dapat memproses hingga 31 transaksi dalam waktu 1 detik.   Saya akan menyebutnya sebagai 31 TPS.   Perhitungannya adalah 91.190 baris dibagi dengan 49 x 60 = 2940 detik.   Transaksi disini bukanlah transaksi dalam arti operasi database, melainkan satu kesatuan operasi membuat invoice baru serta implikasinya (pada pembayaran dan inventory).  Termasuk juga didalamnya operasi mencari suplier, konsumen atau produk berdasarkan kode.   Dengan demikian, sebuah transaksi akan terdiri atas lebih dari satu operasi SQL.

Percobaan 2

Walaupun saya yakin pengguna tidak akan men-klik setiap menu sebanyak 31 kali per detik, tapi angka tersebut cukup kecil.   Saya pun menyelidiki kenapa aplikasi Griffon ini begitu lambat.  Pada akhirnya saya menemukan jawabannya:

  1. Kode program memproses setiap baris data dalam looping.  Keseluruh looping ini akan dianggap sebagai satu transaction (disini saya merujuk pada database transaction).
  2. EntityManager selaku first level cache akan menampung data hasil query SELECT.   Beberapa query men-trigger terjadinya flushing.
  3. Semakin banyak data yang ada di first level cache, maka semakin lama proses validasi yang timbul akibat flushing yang di-trigger secara otomatis.

Untuk mengatasinya, saya mengubah struktur program dari:

...
beginTransaction()
baris.each {
   ... // baca data
   Product product = findProductByCode(productCode)
   Invoice invoice = new Invoice()
   invoice.tambahItem(...)
   if (bayar) {
      invoice.bayar(...)
   }
}
commitTransaction()
...

menjadi:

...
def total = 0
beginTransaction()
baris.each {
   ... // baca data
   Product product = findProductByCode(productCode)
   Invoice invoice = new Invoice()
   invoice.tambahItem(...)
   if (bayar) {
      invoice.bayar(...)
   }
   total++
   if (total%20000==0) clear() // memanggil EntityManager.clear()
}
commitTransaction()
...

Setelah perubahan, saya menemukan bahwa aplikasi tersebut dapat menyelesaikan tugasnya dalam waktu 6 menit.   Hal ini berarti meningkat dari 31 TPS menjadi 253 TPS.   Ini adalah peningkatan yang cukup drastis.

Kesimpulannya:  kode program business logic dengan Groovy terlihat memiliki kinerja yang wajar.   Bahasa dinamis memang lebih lambat dari bahasa statis, tapi seberapa besar dampaknya?   Kode program aplikasi ini juga memakai dynamic finders di simple-jpa untuk mencari entity berdasarkan kodenya.   Penggunaan method dinamis sering kali dianggap lebih lambat dibanding memanggil method secara biasa. Tapi seberapa besar selisih lambatnya? Hari ini saya menemukan jawabannya: layak dipakai 🙂

Percobaan 3

Perjalanan belum selesai!   Kali ini saya akan mengubah kode program sehingga tidak hanya membaca dan membuat object baru di memori, tapi juga menuliskan hasil proses ke database.   Kali ini, saya perlu men-flush() EntityManager setiap 1000 kali baris diproses, sebelum men-clear() EntityManager tersebut.   Perintah flush() akan memaksa Hibernate untuk menuliskan perubahan tersebut ke database dengan melakukan eksekusi SQL.   Walaupun demikian, perubahan yang permanen tetap akan terjadi hanya setelah perintah commit() diberikan.   Jadi, bila saya membatalkan aplikasi di tengah perjalanan, maka tidak akan ada tabel yang isinya berubah.

Lalu, berapa banyak waktu yang dibutuhkan?  Hasil pengujian menunjukkan bahwa untuk memproses 91.190 baris dibutuhkan waktu selama 38 menit.   Dengan demikian kecepatannya adalah 40 TPS.   Hasilnya adalah 91.190 record di tabel Invoice, 17.484 record di tabel pembayaran, dan 129.049 record di tabel inventory.   Nilai 40 TPS adalah hal wajar dan bisa diterima, tapi masalahnya adalah data yang dihasilkan tidak akurat!!   Banyak baris duplikat ditemukan di tabel inventory.   Mengapa demikian?

Percobaan 4

Hal ini berkaitan dengan penggunaan @Canonical di Groovy.   Keinginan saya untuk mendapatkan ‘kenyamanan’ serba otomatis akhirnya membuat saya memperoleh ‘hukuman’.   simple-jpa memberikan @Canonical pada seluruh domain classes yang ada. Annotation ini akan membuat Groovy menciptakan constructor, equals(), hashCode() dan toString() secara otomatis.   Hibernate (lebih tepatnya Java) akan memakai hashCode() dan equals() untuk membandingkan apakah dua buah entity adalah entity yang sama atau berbeda.   Masalahnya: hashCode() dan equals() yang dihasilkan oleh @Canonical sepertinya tidak memperhatikan atribut pada superclass.   Padahal untuk sebuah entity yang diturunakn dari entity lainnya,  biasanya id atau natural primary key yang bisa dipakai untuk membandingkan kesamaan akan terletak di superclass.   Solusinya, saya perlu membuat isi equals() dan hashCode() secara manual pada domain class yang bermasalah.

Setelah dijalankan ulang, tidak ada lagi record yang duplikat di tabel inventory.   Saya juga sempat memanfaatkan kesempatan untuk meningkatkan efisiensi kode program.   Hasil akhirnya, seluruh baris bisa diproses dalam waktu 32 menit (47 TPS).   Data yang dihasilkan adalah 91.190 record di tabel invoice, 17.485 record di tabel pembayaran, dan 73.413 record di tabel inventory.

Kecepatan ini sudah lebih dari cukup mengingat ini adalah aplikasi desktop yang dipakai oleh pengguna tunggal dan hanya sesekali berinteraksi dengan sistem lainnya.

Percobaan 5

Apakah masih bisa lebih cepat?  simple-jpa 0.4.2 memungkinkan pengaturan flush mode untuk EntityManager secara global.   Nilai yang diperboleh adalah "AUTO" atau "COMMIT".   Secara default, di Hibernate JPA, nilainya adalah "AUTO".   Nilai ini menyebabkan Hibernate terkadang akan men-flush() secara otomatis.   Pada saat memanggil sebuah query,  Hibernate bisa saja akan men-flush() bila dirasa perlu.   Bila nilai flush mode adalah "COMMIT", maka proses flush() hanya akan dikerjakan pada saat transaksi di-commit() atau saat flush() dipanggil secara manual.   Pada dokumentasi JPA, disebutkan bahwa nilai "COMMIT" tidak disarankan karena terdapat kemungkinan menghasilkan data yang tidak konsisten. Tapi tidak ada salahnya saya mencoba.

Untuk itu, saya menambahkan baris berikut ini di Config.groovy:

griffon.simplejpa.entityManager.defaultFlushMode = "COMMIT"

Saya cukup terkejut melihat kecepatannya meningkat drastis.   Bahkan saya bisa dengan aman membuang flush() dan clear() yang tadinya secara periodik membersihkan EntityManager agar tidak lambat.   Seluruh perubahan akan diproses ke dalam memori terlebih dahulu tanpa menyentuh database (butuh memori yang besar?), lalu setelah selesai, mereka baru dimasukkan ke database secara keseluruhan.   Hasilnya? Transaksi dapat diselesaikan dalam waktu 8 menit (190 TPS)!   Atau dengan kata lain, untuk jangka waktu 1 detik, aplikasi yang saya uji ini dapat memproses hingga maksimal 190 penyimpanan data invoice baru termasuk segala business process-nya. Ini adalah nilai yang cukup menggembirakan.

Memahami Penggunaan Memori Di Java

Seorang pembaca bertanya pada saya mengapa jumlah memori yang dipakai oleh aplikasi Java-nya besar sekali.  Ia menjelaskan bahwa ia mengetahui hal ini dengan memakai task manager di sistem operasi Windows.  Sebagai contoh, saya menjalankan sebuah aplikasi Griffon yang memakai plugin simple-jpa, mensimulasikan transaksi faktur dengan data real, lalu membuka task manager:

Tampilan Sebuah Program Java Di Task Manager Windows

Tampilan Sebuah Program Java Di Task Manager Windows

Untuk menjawab pertanyaan ini, saya ingin mengingatkan kembali mengenai Java Virtual Machine (JVM).  Tanpa JVM, program Java (dalam bentuk JAR) tidak akan bisa dijalankan.  Lalu apa itu JVM?  JVM adalah sebuah virtual machine!  Maksudnya?  Coba lihat VirtualBox, Microsoft Virtual PC, VMware , dan sebagainya!  Mereka adalah virtual machine yang mensimulasikan sebuah mesin sehingga pengguna Windows dapat menjalankan Linux atau MacOS di komputer mereka.  Begitu juga sebaliknya, dengan virtual machine, pengguna Mac dapat menjalankan sistem operasi Windows.  JVM bukanlah sebuah virtual machine untuk mensimulasikan OS, tapi konsep dasarnya tidak jauh berbeda, sehingga memungkinkan sebuah program Java dijalankan di berbagai platform berbeda.  JVM adalah virtual machine yang khusus menjalankan bytecode yang merupakan hasil kompilasi dari bahasa pemograman Java, Groovy, Scala, JRuby, Jython, dan sebagainya.

Itu sebabnya pada aplikasi Griffon yang saya jalankan, yang terlihat ada sebuah proses javaw.exe.  Ini adalah JVM, bukan program saya dalam bentuk yang dalam jar.  Dengan demikian, akan tidak fair bila membandingkan sebuah JVM dengan sebuah kode program native (.exe).  Program saya belum tentu memakai memori sebanyak itu!

Sama seperti pengguna VirtualBox atau VMware dapat menkonfigurasi jumlah memori yang dipakai, JVM juga memungkinkan konfigurasi memori.  Tapi sebelumnya, lebih baik saya melihat terlebih dahulu penggunaan memori program saya yang sesungguhnya.  Untuk itu, saya membuka lokasi direktori berikut ini di Windows Explorer: C:\Program Files\Java\jdk1.7.0_21\bin.  Di folder ini saya akan menemukan sebuah program bernama jvisualvm.exe.  Saya segera menjalankan program tersebut.  Java VisualVM adalah program bawaan JDK yang memungkinkan developer untuk melihat penggunaan memori aplikasi Java secara detail.  Pada Local, terdapat daftar virtual machine Java yang sedang berjalan termasuk aplikasi saya.   Saya segera men-double click nama aplikasi saya untuk mulai.

Pada tab Monitor, saya dapat melihat penggunaan CPU dan Heap serta PermGen seperti yang terlihat pada gambar berikut ini:

Tampilan monitor CPU dan Memori

Tampilan monitor CPU dan Memori

Gambar di atas adalah aplikasi yang sudah berjalan 20 menit dalam memproses data transaksi invoice tanpa henti.  Memori Heap adalah memori sementara, misalnya nilai variabel lokal ditampung disini.  Dari 98 MB, heap yang terpakai baru 63 MB.   Dari konfigurasi saya, terlihat bahwa maksimal memori komputer yang boleh dipakai sebagai heap adalah 268 MB.  Nilai ini adalah nilai yang kecil bila dibandingkan dengan jumlah memori komputer zaman sekarang (rata-rata 2 hingga 4 GB).

Pada gambar di atas juga terlihat bahwa garbage collector sempat bekerja menjelang jam 14:40.  Garbage collector adalah fitur Java VM yang akan mencari dan membuang object-object yang sudah tak terpakai lagi.

Selain heap, di Java VM juga ada PermGen.  Ini adalah bagian memori yang khusus untuk menampung data yang permanen, seperti informasi struktur class termasuk nilai variabel statis.

Tampilan monitor PermGen

Tampilan monitor PermGen

Untuk informasi yang lebih detail, saya dapat men-klik tab Sampler.  Lalu saya men-klik tombol Memory.  Disini saya dapat melihat informasi mengenai object-object yang ditampung di heap, seperti yang terlihat pada gambar berikut:

Tampilan Heap Sampler

Tampilan Heap histogram

Selain itu, saya juga bisa melihat informasi yang sama untuk PermGen:

Tampilan PermGen histogram

Tampilan PermGen histogram

Pertanyaan berikutnya, alokasi heap saat ini adalah 99 MB dan alokasi PermGen saat ini adalah 26 MB.  Ingat bahwa dari memori yang dialokasikan, tidak semunya terpakai!  Bila ditotalkan, harusnya adalah 125 MB.  Tapi tampilan di task manager adalah 146 MB.  Kemana sisanya?  Java VM (JVM) juga butuh memori untuk keperluan dirinya sendiri, misalnya untuk menampung class-class di JAR yang dibaca.

Dengan memakai tools Windows seperti Sysinternals Process Explorer, saya bisa melihat riwayat penggunaan memori JVM seperti pada gambar berikut ini:

Performance Graph dari Process Explorer

Contoh Tampilan Performance Graph dari Process Explorer

Apakah saya bisa melihat secara detail penggunaan memori di Java VM?  Sama seperti saat saya melihat secara detail penggunaan memori di aplikasi saya sebelumnya?  Iya, bisa, dengan menggunakan Windows SDK dan syaratnya harus memiliki debugging symbol Java VM.  Hal ini bukanlah hal yang menarik untuk dilakukan, apalagi dari gambar di atas tidak terlihat ada kebocoran memori.

Kesimpulannya:  Penggunaan memori untuk aplikasi Java memang lebih besar dibandingkan dengan aplikasi native (exe).  Akan tetapi jumlah pemakaiannya tidak terlalu berlebihan dan masih dalam batas kewajaran (ditambah lagi dengan trend hardware zaman sekarang yang mendukung virtualisasi dan cloudisasi), terutama bila dibandingkan dengan kelebihan Java: kemudahan pengembangan dan portabilitas.