├── LICENSE ├── README.md ├── iibench-0.0.1-1.rockspec ├── iibench.lua ├── iibench_common.lua ├── index_stat.lua └── thread_groups.lua /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-2022 Dmitrii Maximenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sysbench-iibench 2 | 3 | This is a [sysbench](https://github.com/akopytov/sysbench) implementation of the [iiBench Benchmark](https://github.com/tmcallaghan/iibench-mysql). The implementation is based on Mark Callaghan's [python script](https://github.com/mdcallag/mytools/blob/master/bench/ibench/iibench.py). 4 | 5 | ## Synopsis 6 | 7 | The purpose of iiBench is to stress indexed insertion code paths in a database. You can read more about its history and design choices [here](http://www.acmebenchmarking.com/2014/11/announcing-iibench-for-mysql-in-java.html) and [here](http://smalldatum.blogspot.com/2017/06/the-insert-benchmark.html). 8 | This implementation follows the original design and mimics the command line options closely, but provides some nice improvements and extensions on top of it, namely: 9 | - sysbench as the only dependency 10 | - common interface and results format with other sysbench workloads such as OLTP or [TPC-C](https://github.com/Percona-Lab/sysbench-tpcc) 11 | - no Python-induced scalability issues 12 | - the warmup stage 13 | - index caching statistics reported after the warmup 14 | - multiple tables 15 | - multiple insert threads 16 | 17 | # Installation 18 | 19 | To install sysbench use [these](https://github.com/akopytov/sysbench#installing-from-binary-packages) instructions. 20 | 21 | You can then either use iiBench without installation directly from the source repository: 22 | ```shell 23 | sysbench ./iibench.lua ... 24 | ``` 25 | 26 | or install it from the source repository: 27 | ```shell 28 | luarocks make --lua-version=5.1 --local 29 | ``` 30 | 31 | or install it from [SysbenchRocks](https://rocks.sysbench.io/): 32 | ```shell 33 | luarocks --lua-version=5.1 --server=rocks.sysbench.io --local install iibench 34 | ``` 35 | 36 | To get a summary of command line options after installation: 37 | ```shell 38 | sysbench iibench help 39 | ``` 40 | 41 | # Usage 42 | 43 | ## Dataset 44 | 45 | The dataset consists of the specified number of tables with the following schema: 46 | 47 | ```sql 48 | CREATE TABLE sbtest%d( 49 | transactionid BIGINT NOT NULL AUTO_INCREMENT, 50 | dateandtime DATETIME NOT NULL, 51 | cashregisterid INT NOT NULL, 52 | customerid INT NOT NULL, 53 | productid INT NOT NULL, 54 | price FLOAT NOT NULL, 55 | data VARCHAR(%d) NOT NULL, 56 | PRIMARY KEY (transactionid) 57 | ) 58 | ``` 59 | 60 | There is also up to 3 secondary indexes in each table: on `(price, customerid)`, `(cashregisterid, price, customerid)` and `(price, dateandtime, customerid)`. All three indexes are created by default, but that can be changed with the `--num-secondary-indexes` option. 61 | 62 | The number of rows in each table is specified with `--table-size` (10000 by default). 63 | 64 | Tables can be optionally partitioned with a specified number of partitions and rows per partition using the following options: 65 | - `--num-partitions` (0 by default) 66 | - `--rows-per-partition` (0 by default, in which case is computed as `table_size / num_partitions`) 67 | 68 | The ranges of values in numeric columns are controlled by the following options: 69 | - `--cashregisters` (1000 by default) 70 | - `--products` (10 000 by default) 71 | - `--customers` (100 000 by default) 72 | - `--max-price` (500 by default) 73 | 74 | Strings in the `data` column are controlled by the following options: 75 | - `--data-length-min` (10 by default) 76 | - `--data-length-max` (10 by default) which is also used as the `VARCHAR` length 77 | - `--data-random-pct` (50 by default) specifies the length in percent of the randomly generated suffix for each string as opposed to static prefix 78 | 79 | ## Threads and rates 80 | 81 | The `--insert-threads` option sets the number of threads executing `INSERT`s (defaults to 1). 82 | 83 | The number of `SELECT` threads is calculated as `--threads` (which is the total number of threads created by sysbench) minus `--insert-threads`. 84 | 85 | `--select-rate` and `--insert-rate` can be used to control the number of queries per second executed by each `SELECT` and `INSERT` thread, respectively. The default for both options is 0, which means no rate limit. Alternatively, the `--insert-per-second` option can be used to specify a total limit for all `INSERT` threads. 86 | 87 | ## Limiting table size 88 | 89 | The initial table size is specified with `--table-size`. Since this is an `INSERT`-mostly workload, you may want to place a limit on how big the tables are allowed to grow. 90 | 91 | To do so, use `--with-max-table-rows=true --max-table-rows=N`, in which case the workload will start deleting oldest rows after reaching `N` rows, i.e. after inserting `max_tables_rows - table_size` rows. 92 | 93 | This behavior is disabled by default. 94 | 95 | ## Examples 96 | 97 | ### Prepare data and tables 98 | ```shell 99 | sysbench iibench --mysql-socket=/tmp/mysql.sock --mysql-user=root --threads=10 --tables=10 --table-size=1000000 --num-secondary-indexes=2 --db-driver=mysql prepare 100 | ``` 101 | ### Warm up 102 | ```shell 103 | sysbench iibench --mysql-socket=/tmp/mysql.sock --tables=10 --table-size=1000000 --stat=true warmup 104 | ``` 105 | ### Run benchmark 106 | ```shell 107 | sysbench iibench --mysql-socket=/tmp/mysql.sock --mysql-user=root --time=300 --threads=64 --report-interval=1 --tables=10 \ 108 | --table-size=1000000 --insert-threads=8 --inserts-per-second=50 --num-secondary-indexes=2 --db-driver=mysql run 109 | ``` 110 | ### Cleanup 111 | ```shell 112 | sysbench iibench --mysql-socket=/tmp/mysql.sock --mysql-user=root --threads=10 --db-driver=mysql cleanup 113 | ``` 114 | 115 | -------------------------------------------------------------------------------- /iibench-0.0.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "iibench" 2 | version = "0.0.1-1" 3 | source = { 4 | url = "git+https://github.com/Dmitree-Max/sysbench-iibench" 5 | } 6 | 7 | description = { 8 | summary = 9 | "This is the iiBench benchmark (aka the Index Insertion Benchmark) implemented as a sysbench workload", 10 | detailed = [[ 11 | # sysbench-iibench 12 | 13 | This is a [sysbench](https://github.com/akopytov/sysbench) implementation of the [iiBench Benchmark](https://github.com/tmcallaghan/iibench-mysql). The implementation is based on Mark Callaghan's [python script](https://github.com/mdcallag/mytools/blob/master/bench/ibench/iibench.py). 14 | 15 | ## Synopsis 16 | 17 | The purpose of iiBench is to stress indexed insertion code paths in a database. You can read more about its history and design choices [here](http://www.acmebenchmarking.com/2014/11/announcing-iibench-for-mysql-in-java.html) and [here](http://smalldatum.blogspot.com/2017/06/the-insert-benchmark.html). 18 | This implementation follows the original design and mimics the command line options closely, but provides some nice improvements and extensions on top of it, namely: 19 | - sysbench as the only dependency 20 | - common interface and results format with other sysbench workloads such as OLTP or [TPC-C](https://github.com/Percona-Lab/sysbench-tpcc) 21 | - no Python-induced scalability issues 22 | - the warmup stage 23 | - index caching statistics reported after the warmup 24 | - multiple tables 25 | - multiple insert threads 26 | 27 | # Installation 28 | 29 | To install sysbench use [these](https://github.com/akopytov/sysbench#installing-from-binary-packages) instructions. 30 | 31 | You can then either use iiBench without installation directly from the source repository: 32 | ```shell 33 | sysbench ./iibench.lua ... 34 | ``` 35 | 36 | or install it from the source repository: 37 | ```shell 38 | luarocks make --lua-version=5.1 --local 39 | ``` 40 | 41 | or install it from [SysbenchRocks](https://rocks.sysbench.io/): 42 | ```shell 43 | luarocks --lua-version=5.1 --server=rocks.sysbench.io --local install iibench 44 | ``` 45 | 46 | To get a summary of command line options after installation: 47 | ```shell 48 | sysbench iibench help 49 | ``` 50 | 51 | # Usage 52 | 53 | ## Dataset 54 | 55 | The dataset consists of the specified number of tables with the following schema: 56 | 57 | ```sql 58 | CREATE TABLE sbtest%d( 59 | transactionid BIGINT NOT NULL AUTO_INCREMENT, 60 | dateandtime DATETIME NOT NULL, 61 | cashregisterid INT NOT NULL, 62 | customerid INT NOT NULL, 63 | productid INT NOT NULL, 64 | price FLOAT NOT NULL, 65 | data VARCHAR(%d) NOT NULL, 66 | PRIMARY KEY (transactionid) 67 | ) 68 | ``` 69 | 70 | There is also up to 3 secondary indexes in each table: on `(price, customerid)`, `(cashregisterid, price, customerid)` and `(price, dateandtime, customerid)`. All three indexes are created by default, but that can be changed with the `--num-secondary-indexes` option. 71 | 72 | The number of rows in each table is specified with `--table-size` (10000 by default). 73 | 74 | Tables can be optionally partitioned with a specified number of partitions and rows per partition using the following options: 75 | - `--num-partitions` (0 by default) 76 | - `--rows-per-partition` (0 by default, in which case is computed as `table_size / num_partitions`) 77 | 78 | The ranges of values in numeric columns are controlled by the following options: 79 | - `--cashregisters` (1000 by default) 80 | - `--products` (10 000 by default) 81 | - `--customers` (100 000 by default) 82 | - `--max-price` (500 by default) 83 | 84 | Strings in the `data` column are controlled by the following options: 85 | - `--data-length-min` (10 by default) 86 | - `--data-length-max` (10 by default) which is also used as the `VARCHAR` length 87 | - `--data-random-pct` (50 by default) specifies the length in percent of the randomly generated suffix for each string as opposed to static prefix 88 | 89 | ## Threads and rates 90 | 91 | The `--insert-threads` option sets the number of threads executing `INSERT`s (defaults to 1). 92 | 93 | The number of `SELECT` threads is calculated as `--threads` (which is the total number of threads created by sysbench) minus `--insert-threads`. 94 | 95 | `--select-rate` and `--insert-rate` can be used to control the number of queries per second executed by each `SELECT` and `INSERT` thread, respectively. The default for both options is 0, which means no rate limit. Alternatively, the `--insert-per-second` option can be used to specify a total limit for all `INSERT` threads. 96 | 97 | ## Limiting table size 98 | 99 | The initial table size is specified with h`--table-size`. Since this is an `INSERT`-mostly workload, you may want to place a limit on how big the tables are allowed to grow. 100 | 101 | To do so, use `--with-max-table-rows=true --max-table-rows=N`, in which case the workload will start deleting oldest rows after reaching `N` rows, i.e. after inserting `max_tables_rows - table_size` rows. 102 | 103 | This behavior is disabled by default. 104 | 105 | ## Examples 106 | 107 | ### Prepare data and tables 108 | ```shell 109 | sysbench iibench --mysql-socket=/tmp/mysql.sock --mysql-user=root --threads=10 --tables=10 --table-size=1000000 --num-secondary-indexes=2 --db-driver=mysql prepare 110 | ``` 111 | ### Warm up 112 | ```shell 113 | sysbench iibench --mysql-socket=/tmp/mysql.sock --tables=10 --table-size=1000000 --stat=true warmup 114 | ``` 115 | ### Run benchmark 116 | ```shell 117 | sysbench iibench --mysql-socket=/tmp/mysql.sock --mysql-user=root --time=300 --threads=64 --report-interval=1 --tables=10 \ 118 | --table-size=1000000 --insert-threads=8 --inserts-per-second=50 --num-secondary-indexes=2 --db-driver=mysql run 119 | ``` 120 | ### Cleanup 121 | ```shell 122 | sysbench iibench --mysql-socket=/tmp/mysql.sock --mysql-user=root --threads=10 --db-driver=mysql cleanup 123 | ``` 124 | ]], 125 | homepage = "https://github.com/Dmitree-Max/sysbench-iibench", 126 | license = "MIT" 127 | } 128 | 129 | dependencies = { 130 | "lua == 5.1" 131 | } 132 | 133 | build = { 134 | type = "builtin", 135 | modules = { 136 | iibench = "iibench.lua", 137 | index_stat = "index_stat.lua", 138 | iibench_common = "iibench_common.lua", 139 | thread_groups = "thread_groups.lua" 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /iibench.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sysbench 2 | -- Copyright (C) 2020-2022 Dmitrii Maximenko 3 | 4 | -- Use of this source code is governed by an MIT-style 5 | -- license that can be found in the LICENSE file or at 6 | -- https://opensource.org/licenses/MIT 7 | 8 | -- ---------------------------------------------------------------------- 9 | -- Index insertion benchmark 10 | -- ---------------------------------------------------------------------- 11 | 12 | require("iibench_common") 13 | 14 | 15 | function prepare_statements() 16 | if sysbench.opt.threads - sysbench.opt.insert_threads > 0 17 | then 18 | prepare_market_queries() 19 | prepare_register_queries() 20 | prepare_pdc_queries() 21 | end 22 | prepare_thread_groups() 23 | if sysbench.opt.instant_delete 24 | then 25 | prepare_deletes() 26 | end 27 | end 28 | 29 | function insert_event() 30 | execute_inserts() 31 | 32 | check_reconnect() 33 | end 34 | 35 | function event() 36 | local query_type = sysbench.rand.uniform(1,3) 37 | local switch = { 38 | [1] = execute_market_queries, 39 | [2] = execute_pdc_queries, 40 | [3] = execute_register_queries 41 | } 42 | switch[query_type]() 43 | 44 | 45 | check_reconnect() 46 | end 47 | 48 | 49 | function prepare_thread_groups() 50 | 51 | -- resolve alias 52 | if sysbench.opt.query_threads ~= 0 53 | then 54 | if sysbench.opt.select_threads == 0 55 | then 56 | sysbench.opt.select_threads = sysbench.opt.query_threads 57 | else 58 | print('query_threads is an alias for select_threads, but they have ' .. 59 | 'different values') 60 | end 61 | end 62 | 63 | if sysbench.opt.query_rate ~= 0 64 | then 65 | if sysbench.opt.select_rate == 0 66 | then 67 | sysbench.opt.select_rate = sysbench.opt.query_rate 68 | else 69 | print('query_rate is an alias for select_rate, but they have ' .. 70 | 'different values') 71 | end 72 | end 73 | 74 | 75 | local insert_rate = sysbench.opt.inserts_per_second / sysbench.opt.insert_threads 76 | if insert_rate == 0 77 | then 78 | insert_rate = sysbench.opt.insert_rate 79 | end 80 | 81 | thread_groups = { 82 | { 83 | event = insert_event, 84 | thread_amount = sysbench.opt.insert_threads, 85 | rate = insert_rate, 86 | rate_controller = default_rate_controller 87 | }, 88 | { 89 | event = event, 90 | thread_amount = sysbench.opt.select_threads, 91 | rate = sysbench.opt.select_rate, 92 | rate_controller = default_rate_controller 93 | } 94 | } 95 | end -------------------------------------------------------------------------------- /iibench_common.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2020-2022 Dmitrii Maximenko 2 | 3 | -- Use of this source code is governed by an MIT-style 4 | -- license that can be found in the LICENSE file or at 5 | -- https://opensource.org/licenses/MIT 6 | 7 | -- ----------------------------------------------------------------------------- 8 | -- Common code for iiBench (Index insertion benchmark) benchmarks. 9 | -- ----------------------------------------------------------------------------- 10 | 11 | 12 | require("thread_groups") 13 | require("index_stat") 14 | 15 | function init() 16 | -- assert(sysbench.version >= '1.1.1', "There is a bug with preparing float values in" .. 17 | -- " sysbench versions under 1.1.1. Please use iibench with newer sysbench version.") 18 | 19 | assert(event ~= nil, 20 | "this script is meant to be included by other scripts and " .. 21 | "should not be called directly.") 22 | 23 | end 24 | 25 | if sysbench.cmdline.command == nil then 26 | error("Command is required. Supported commands: prepare, warmup, run, " .. 27 | "cleanup, help") 28 | end 29 | 30 | -- Command line options 31 | sysbench.cmdline.options = { 32 | 33 | -- Subject field parameters 34 | cashregisters = 35 | {"# cash registers", 1000}, 36 | products = 37 | {"# products", 10000}, 38 | customers = 39 | {"# customers", 100000}, 40 | max_price = 41 | {"Maximum value for price column", 500}, 42 | 43 | 44 | -- Query parameters 45 | rows_per_commit = 46 | {"rows inserted per transaction", 1000}, 47 | rows_per_query = 48 | {"limit of selected rows", 10}, 49 | data_length_max = 50 | {"Max size of data in data column", 10}, 51 | data_length_min = 52 | {"Min size of data in data column", 10}, 53 | data_random_pct = 54 | {"Percentage of row that has random data", 50}, 55 | secondary_at_end = 56 | {"Create secondary index at end", false}, 57 | insert_rate = 58 | {"Average rate for inserts", 0}, 59 | inserts_per_second = 60 | {"Average rate for all insert threads at once", 0}, 61 | select_rate = 62 | {"Average rate for selects", 0}, 63 | query_rate = 64 | {"An alias for select_rate", 0}, 65 | insert_threads = 66 | {"Amount of threads for inserts", 1}, 67 | select_threads = 68 | {"Amount of threads for selects, default 0 means all others", 0}, 69 | query_threads = 70 | {"An alias for 'select_threads'", 0}, 71 | 72 | 73 | -- Table parameters 74 | table_size = 75 | {"Number of rows in table", 10000}, 76 | max_table_rows = 77 | {"Max number of rows in table", 10000000}, 78 | num_secondary_indexes = 79 | {"Number of secondary indexes (0 to 3)", 3}, 80 | num_partitions = 81 | {"Use range partitioning when not 0", 0}, 82 | rows_per_partition = 83 | {"Number of rows per partition. If 0 this is computed as max_rows/num_partitions", 0}, 84 | fill_table = 85 | {"Put table_size rows in table", true}, 86 | with_max_table_rows = 87 | {"Do we want delete after insert, when table more than max size", false}, 88 | 89 | 90 | -- other options 91 | tables = 92 | {"Number of tables", 1}, 93 | reconnect = 94 | {"Reconnect after every N events. The default (0) is to not reconnect", 95 | 0}, 96 | stat = 97 | {"This option controls statistic print for warm up command", 98 | false}, 99 | mysql_storage_engine = 100 | {"Storage engine, if MySQL is used", "innodb"}, 101 | create_table_options = 102 | {"Options passed to CREATE TABLE statement", ""}, 103 | pgsql_variant = 104 | {"Use this PostgreSQL variant when running with the " .. 105 | "PostgreSQL driver. The only currently supported " .. 106 | "variant is 'redshift'. When enabled, " .. 107 | "create_secondary is automatically disabled, and " .. 108 | "delete_inserts is set to 0"} 109 | } 110 | 111 | -- Prepare the dataset. This command supports parallel execution, i.e. will 112 | -- benefit from executing with --threads > 1 as long as --tables > 1 113 | function cmd_prepare() 114 | local drv = sysbench.sql.driver() 115 | local con = drv:connect() 116 | 117 | for i = sysbench.tid % sysbench.opt.threads + 1, sysbench.opt.tables, 118 | sysbench.opt.threads do 119 | create_table(drv, con, i) 120 | create_index(drv, con, i) 121 | end 122 | end 123 | 124 | -- Preload the dataset into the server cache. This command supports parallel 125 | -- execution, i.e. will benefit from executing with --threads > 1 as long as 126 | -- --tables > 1 127 | -- 128 | -- PS. Currently, this command is only meaningful for MySQL/InnoDB benchmarks 129 | function cmd_warmup() 130 | 131 | local drv = sysbench.sql.driver() 132 | local con = drv:connect() 133 | 134 | assert(drv:name() == "mysql", "warmup is currently MySQL only") 135 | 136 | -- Do not create on disk tables for subsequent queries 137 | con:query("SET tmp_table_size=2*1024*1024*1024") 138 | con:query("SET max_heap_table_size=2*1024*1024*1024") 139 | 140 | 141 | 142 | for i = sysbench.tid % sysbench.opt.threads + 1, sysbench.opt.tables, 143 | sysbench.opt.threads do 144 | local t = "sbtest" .. i 145 | print("Preloading table " .. t) 146 | 147 | if (sysbench.opt.stat) 148 | then 149 | print("\nstat before warmup:") 150 | show_index_stat(string.format("sbtest%u", i)) 151 | end 152 | con:query("ANALYZE TABLE sbtest" .. i) 153 | 154 | 155 | 156 | --96,76 157 | 158 | con:query(string.format( 159 | "SELECT AVG(transactionid) FROM " .. 160 | "(SELECT * FROM %s FORCE KEY (PRIMARY) " .. 161 | "LIMIT %u) t", 162 | t, sysbench.opt.table_size)) 163 | 164 | 165 | 166 | con:query(string.format( 167 | "SELECT COUNT(*) FROM " .. 168 | "(SELECT * FROM %s WHERE data LIKE '%%0%%' LIMIT %u) t", 169 | t, sysbench.opt.table_size)) 170 | 171 | if sysbench.opt.num_secondary_indexes > 0 172 | then 173 | con:query(string.format( 174 | "SELECT COUNT(*) FROM " .. 175 | " %s FORCE KEY ( %s_marketsegment ) WHERE customerid like '%%0%%'", 176 | t, t)) 177 | end 178 | if sysbench.opt.num_secondary_indexes > 1 179 | then 180 | con:query(string.format( 181 | "SELECT COUNT(*) FROM " .. 182 | " %s FORCE KEY ( %s_registersegment ) WHERE cashregisterid like '%%0%%'", 183 | t, t)) 184 | end 185 | if sysbench.opt.num_secondary_indexes > 2 186 | then 187 | con:query(string.format( 188 | "SELECT COUNT(*) FROM " .. 189 | " %s FORCE KEY ( %s_pdc ) WHERE dateandtime like '%%0%%'", 190 | t, t)) 191 | end 192 | 193 | if (sysbench.opt.stat) 194 | then 195 | print("\nstat after warmup:") 196 | show_index_stat(string.format("sbtest%u", i)) 197 | end 198 | 199 | 200 | end 201 | 202 | end 203 | 204 | -- Implement parallel prepare and warmup commands, define 'prewarm' as an alias 205 | -- for 'warmup' 206 | sysbench.cmdline.commands = { 207 | prepare = {cmd_prepare, sysbench.cmdline.PARALLEL_COMMAND}, 208 | warmup = {cmd_warmup, sysbench.cmdline.PARALLEL_COMMAND}, 209 | prewarm = {cmd_warmup, sysbench.cmdline.PARALLEL_COMMAND} 210 | } 211 | 212 | 213 | function create_table(drv, con, table_num) 214 | local id_def 215 | local engine_def = "/*! ENGINE = " .. sysbench.opt.mysql_storage_engine .. " */" 216 | local extra_table_options = "" 217 | local query 218 | local partition_options = "" 219 | local rows_per_part = 0 220 | 221 | if (sysbench.opt.num_partitions > 0) then 222 | if (sysbench.opt.rows_per_partition > 0) then 223 | rows_per_part = sysbench.opt.rows_per_partition 224 | else 225 | rows_per_part = sysbench.opt.table_size / sysbench.opt.num_partitions 226 | end 227 | 228 | partition_options = "partition by range( transactionid ) (" 229 | 230 | for i = 1, sysbench.opt.num_partitions - 1 do 231 | partition_options = partition_options .. 232 | string.format(" partition p%d values less than (%d),\n", i, i*rows_per_part) 233 | end 234 | partition_options = partition_options .. 235 | string.format(" partition p%d values less than (MAXVALUE)\n)", sysbench.opt.num_partitions) 236 | end 237 | 238 | print(string.format("Creating table 'sbtest%d'...", table_num)) 239 | 240 | query = string.format([[ 241 | CREATE TABLE sbtest%d( 242 | transactionid BIGINT NOT NULL AUTO_INCREMENT, 243 | dateandtime datetime NOT NULL, 244 | cashregisterid int NOT NULL, 245 | customerid int NOT NULL, 246 | productid int NOT NULL, 247 | price float NOT NULL, 248 | data varchar(%d) NOT NULL, 249 | primary key (transactionid) 250 | ) %s %s %s 251 | ]], 252 | table_num, sysbench.opt.data_length_max, engine_def, sysbench.opt.create_table_options, partition_options) 253 | 254 | con:query(query) 255 | 256 | if (sysbench.opt.table_size > 0) then 257 | print(string.format("Inserting %d records into 'sbtest%d'", 258 | sysbench.opt.table_size, table_num)) 259 | end 260 | 261 | if sysbench.opt.fill_table then 262 | query = string.format([[INSERT INTO sbtest%d(dateandtime, cashregisterid, customerid, productid, price, data) 263 | VALUES]], table_num) 264 | end 265 | 266 | con:bulk_insert_init(query) 267 | 268 | local c_val 269 | local pad_val 270 | if sysbench.opt.fill_table then 271 | for i = 1, sysbench.opt.table_size do 272 | query = make_insert_query_string() 273 | con:bulk_insert_next(query) 274 | end 275 | end 276 | 277 | con:bulk_insert_done() 278 | end 279 | 280 | function create_index(drv, con, table_num) 281 | if (sysbench.opt.num_secondary_indexes > 0) then 282 | local index_ddl = string.format("alter table sbtest%d add index sbtest%d_marketsegment (price, customerid) ", 283 | table_num, table_num, table_num) 284 | if (sysbench.opt.num_secondary_indexes > 1) then 285 | index_ddl = index_ddl .. 286 | string.format(", add index sbtest%d_registersegment (cashregisterid, price, customerid) ", 287 | table_num, table_num) 288 | if (sysbench.opt.num_secondary_indexes > 2) then 289 | index_ddl = index_ddl .. 290 | string.format(", add index sbtest%d_pdc (price, dateandtime, customerid)", 291 | table_num, table_num) 292 | end 293 | end 294 | print(string.format("Creating %d indexes into 'sbtest%d'", 295 | sysbench.opt.num_secondary_indexes, table_num)) 296 | con:query(index_ddl) 297 | end 298 | end 299 | 300 | 301 | function make_insert_query_string() 302 | return string.format("('%s', %d, %d, %d, %f, '%s')", create_insert_data()) 303 | end 304 | 305 | local function get_table_num() 306 | return sysbench.rand.uniform(1, sysbench.opt.tables) 307 | end 308 | 309 | local function get_customerid_id() 310 | return sysbench.rand.default(1, sysbench.opt.customers) 311 | end 312 | 313 | local function get_cashregister_id() 314 | return sysbench.rand.uniform(0, sysbench.opt.cashregisters) 315 | end 316 | 317 | local function get_price(customerid) 318 | return (sysbench.rand.uniform_double() * sysbench.opt.max_price + customerid) / 100.0 319 | end 320 | 321 | function create_insert_data() 322 | 323 | local dateandtime = os.date('%Y-%m-%d %H:%M:%S') 324 | local cashregisterid = get_cashregister_id() 325 | local customerid = get_customerid_id() 326 | local productid = sysbench.rand.uniform(0, sysbench.opt.products) 327 | local price = get_price(customerid) 328 | 329 | local data_length = sysbench.rand.uniform(sysbench.opt.data_length_min, sysbench.opt.data_length_max) 330 | local rand_data_length = math.floor(sysbench.opt.data_random_pct / 100 * data_length) 331 | if data_length == rand_data_length 332 | then 333 | rand_data_length = rand_data_length - 1 -- because last letter is always a 334 | end 335 | local data = string.rep('a', data_length - rand_data_length - 1) .. 336 | sysbench.rand.varstring(rand_data_length, rand_data_length) .. 'a' 337 | 338 | return dateandtime, cashregisterid, customerid, productid, price, data 339 | end 340 | 341 | 342 | local t = sysbench.sql.type 343 | local stmt_defs = { 344 | market_queries = { 345 | [[ 346 | SELECT price, customerid FROM sbtest%u %s 347 | where (price >= ?) ORDER BY price, customerid LIMIT ? 348 | ]], 349 | t.INT, t.INT 350 | }, 351 | register_queries = { 352 | [[ 353 | SELECT cashregisterid,price,customerid FROM sbtest%u %s 354 | where (cashregisterid > ?) ORDER BY cashregisterid,price,customerid LIMIT ? 355 | ]], 356 | t.INT, t.INT 357 | }, 358 | pdc_queries = { 359 | [[ 360 | SELECT price, dateandtime, customerid FROM sbtest%u %s 361 | where (price >= ?) ORDER BY price, dateandtime, customerid LIMIT ? 362 | ]], 363 | t.INT, t.INT 364 | }, 365 | deletes = { 366 | [[ 367 | DELETE FROM sbtest%u ORDER BY transactionid LIMIT ? 368 | ]], t.INT 369 | } 370 | } 371 | 372 | function prepare_begin() 373 | stmt.begin = con:prepare("BEGIN") 374 | end 375 | 376 | function prepare_commit() 377 | stmt.commit = con:prepare("COMMIT") 378 | end 379 | 380 | function prepare_for_each_table(key) 381 | for t = 1, sysbench.opt.tables do 382 | 383 | if key == "deletes" 384 | then 385 | stmt[t][key] = con:prepare(string.format(stmt_defs[key][1], t)) 386 | else 387 | -- ternary operators in Lua: x = condition and opt1 or opt2 388 | local switch = { 389 | ["market_queries"] = (sysbench.opt.num_secondary_indexes > 0) 390 | and string.format("FORCE INDEX (sbtest%u_marketsegment)", t) or "", 391 | ["register_queries"] = (sysbench.opt.num_secondary_indexes > 1) 392 | and string.format("FORCE INDEX (sbtest%u_registersegment)", t) or "", 393 | ["pdc_queries"] = (sysbench.opt.num_secondary_indexes > 2) 394 | and string.format("FORCE INDEX (sbtest%u_pdc)", t) or "" 395 | } 396 | stmt[t][key] = con:prepare(string.format(stmt_defs[key][1], t, switch[key])) 397 | end 398 | 399 | local nparam = #stmt_defs[key] - 1 400 | 401 | if nparam > 0 then 402 | param[t][key] = {} 403 | end 404 | 405 | for p = 1, nparam do 406 | local btype = stmt_defs[key][p+1] 407 | local len 408 | 409 | if type(btype) == "table" then 410 | len = btype[2] 411 | btype = btype[1] 412 | end 413 | if btype == sysbench.sql.type.VARCHAR or 414 | btype == sysbench.sql.type.CHAR then 415 | param[t][key][p] = stmt[t][key]:bind_create(btype, len) 416 | else 417 | param[t][key][p] = stmt[t][key]:bind_create(btype) 418 | end 419 | end 420 | 421 | if nparam > 0 then 422 | stmt[t][key]:bind_param(unpack(param[t][key])) 423 | end 424 | end 425 | end 426 | 427 | 428 | function prepare_deletes() 429 | -- variables for deletes 430 | if (sysbench.opt.with_max_table_rows) 431 | then 432 | insert_count = 0 433 | delete_flag = false 434 | end 435 | 436 | prepare_for_each_table("deletes") 437 | end 438 | 439 | function prepare_market_queries() 440 | prepare_for_each_table("market_queries") 441 | end 442 | 443 | function prepare_pdc_queries() 444 | prepare_for_each_table("pdc_queries") 445 | end 446 | 447 | function prepare_register_queries() 448 | prepare_for_each_table("register_queries") 449 | end 450 | 451 | function thread_init() 452 | drv = sysbench.sql.driver() 453 | con = drv:connect() 454 | 455 | -- Create global nested tables for prepared statements and their 456 | -- parameters. We need a statement and a parameter set for each combination 457 | -- of connection/table/query 458 | stmt = {} 459 | param = {} 460 | 461 | for t = 1, sysbench.opt.tables do 462 | stmt[t] = {} 463 | param[t] = {} 464 | end 465 | 466 | -- This function is a 'callback' defined by individual benchmark scripts 467 | prepare_statements() 468 | end 469 | 470 | -- Close prepared statements 471 | function close_statements() 472 | for t = 1, sysbench.opt.tables do 473 | for k, s in pairs(stmt[t]) do 474 | stmt[t][k]:close() 475 | end 476 | end 477 | if (stmt.begin ~= nil) then 478 | stmt.begin:close() 479 | end 480 | if (stmt.commit ~= nil) then 481 | stmt.commit:close() 482 | end 483 | end 484 | 485 | function thread_done() 486 | close_statements() 487 | con:disconnect() 488 | end 489 | 490 | function cleanup() 491 | local drv = sysbench.sql.driver() 492 | local con = drv:connect() 493 | 494 | for i = 1, sysbench.opt.tables do 495 | print(string.format("Dropping table 'sbtest%d'...", i)) 496 | con:query("DROP TABLE IF EXISTS sbtest" .. i ) 497 | end 498 | end 499 | 500 | 501 | function begin() 502 | stmt.begin:execute() 503 | end 504 | 505 | function commit() 506 | stmt.commit:execute() 507 | end 508 | 509 | 510 | function execute_market_queries() 511 | local tnum = get_table_num() 512 | 513 | local customer_id = get_customerid_id() 514 | local price = get_price(customer_id) 515 | param[tnum].market_queries[1]:set(price) 516 | param[tnum].market_queries[2]:set(sysbench.opt.rows_per_query) 517 | 518 | stmt[tnum].market_queries:execute() 519 | 520 | end 521 | 522 | 523 | function execute_pdc_queries() 524 | local tnum = get_table_num() 525 | 526 | local customer_id = get_customerid_id() 527 | local price = get_price(customer_id) 528 | param[tnum].pdc_queries[1]:set(price) 529 | param[tnum].pdc_queries[2]:set(sysbench.opt.rows_per_query) 530 | 531 | stmt[tnum].pdc_queries:execute() 532 | end 533 | 534 | 535 | function execute_register_queries() 536 | local tnum = get_table_num() 537 | 538 | param[tnum].register_queries[1]:set(get_cashregister_id()) 539 | param[tnum].register_queries[2]:set(sysbench.opt.rows_per_query) 540 | 541 | stmt[tnum].register_queries:execute() 542 | 543 | end 544 | 545 | 546 | function execute_inserts() 547 | local tnum = get_table_num() 548 | local query = string.format([[INSERT INTO sbtest%d(dateandtime, cashregisterid, customerid, productid, price, data) 549 | VALUES]], tnum) 550 | 551 | con:bulk_insert_init(query) 552 | 553 | for i = 1, sysbench.opt.rows_per_commit do 554 | query = make_insert_query_string() 555 | con:bulk_insert_next(query) 556 | end 557 | con:bulk_insert_done() 558 | 559 | if sysbench.opt.with_max_table_rows 560 | then 561 | if delete_flag 562 | then 563 | execute_deletes(tnum) 564 | else 565 | insert_count = insert_count + 1 566 | check_delete_start() 567 | end 568 | end 569 | end 570 | 571 | 572 | function check_delete_start() 573 | if insert_count > (sysbench.opt.max_table_rows - sysbench.opt.table_size) / 574 | (sysbench.opt.rows_per_commit * sysbench.opt.insert_threads / sysbench.opt.tables) 575 | then 576 | delete_flag = true 577 | end 578 | end 579 | 580 | function execute_deletes(tnum) 581 | param[tnum].deletes[1]:set(sysbench.opt.rows_per_commit) 582 | stmt[tnum].deletes:execute() 583 | end 584 | 585 | 586 | -- Re-prepare statements if we have reconnected, which is possible when some of 587 | -- the listed error codes are in the --mysql-ignore-errors list 588 | function sysbench.hooks.before_restart_event(errdesc) 589 | if errdesc.sql_errno == 2013 or -- CR_SERVER_LOST 590 | errdesc.sql_errno == 2055 or -- CR_SERVER_LOST_EXTENDED 591 | errdesc.sql_errno == 2006 or -- CR_SERVER_GONE_ERROR 592 | errdesc.sql_errno == 2011 -- CR_TCP_CONNECTION 593 | then 594 | close_statements() 595 | prepare_statements() 596 | end 597 | end 598 | 599 | function check_reconnect() 600 | if sysbench.opt.reconnect > 0 then 601 | transactions = (transactions or 0) + 1 602 | if transactions % sysbench.opt.reconnect == 0 then 603 | close_statements() 604 | con:reconnect() 605 | prepare_statements() 606 | end 607 | end 608 | end 609 | -------------------------------------------------------------------------------- /index_stat.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2020-2022 Dmitrii Maximenko 2 | 3 | -- Use of this source code is governed by an MIT-style 4 | -- license that can be found in the LICENSE file or at 5 | -- https://opensource.org/licenses/MIT 6 | 7 | 8 | 9 | -- This functions queries information about 10 | -- indexes in innoDB buffer poll and prints it to std out 11 | function show_index_stat(table_name) 12 | local drv = sysbench.sql.driver() 13 | local con = drv:connect() 14 | 15 | con:query("SET SESSION sql_mode = sys.list_drop(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY');") 16 | con:query("SET sql_mode = sys.list_drop(@@sql_mode, 'ONLY_FULL_GROUP_BY');") 17 | con:query("SET @@sql_mode = sys.list_drop(@@sql_mode, 'ONLY_FULL_GROUP_BY');") 18 | 19 | con:query("USE information_schema;") 20 | con:query("SET @page_size = @@innodb_page_size;;") 21 | con:query("SET @bp_pages = @@innodb_buffer_pool_size/@page_size;") 22 | 23 | local query = 24 | [[ 25 | SELECT INDEX_TYPE, PAGES, PCT_OF_BUFFER_POOL, PCT_OF_INDEX 26 | FROM 27 | (SELECT P.TABLE_NAME, P.PAGE_TYPE, 28 | CASE 29 | WHEN P.INDEX_NAME IS NULL 30 | THEN NULL 31 | WHEN P.TABLE_NAME LIKE '`SYS_%' 32 | THEN P.INDEX_NAME 33 | WHEN P.INDEX_NAME <> 'PRIMARY' 34 | THEN 'SECONDARY' 35 | ELSE 36 | 'PRIMARY' 37 | END 38 | AS INDEX_TYPE, 39 | 40 | COUNT(DISTINCT P.PAGE_NUMBER) AS PAGES, 41 | ROUND(100*COUNT(DISTINCT P.PAGE_NUMBER)/@bp_pages,2) 42 | AS PCT_OF_BUFFER_POOL, 43 | CASE 44 | WHEN P.TABLE_NAME IS NULL 45 | THEN NULL 46 | WHEN P.TABLE_NAME LIKE '`SYS_%' 47 | THEN NULL 48 | ELSE 49 | ROUND(100*COUNT(DISTINCT P.PAGE_NUMBER)/ 50 | CASE P.INDEX_NAME 51 | WHEN 'PRIMARY' 52 | THEN TS.DATA_LENGTH/@page_size 53 | ELSE 54 | TS.INDEX_LENGTH/@page_size 55 | END, 56 | 2) 57 | END 58 | AS PCT_OF_INDEX 59 | 60 | FROM INNODB_BUFFER_PAGE AS P 61 | JOIN INNODB_SYS_TABLES AS T 62 | ON P.SPACE = T.SPACE 63 | JOIN TABLES AS TS ON T.NAME = CONCAT(TS.TABLE_SCHEMA,'/',TS.TABLE_NAME) 64 | WHERE TS.TABLE_SCHEMA <> 'mysql' and TS.TABLE_NAME = ]] .. 65 | string.format("'%s'", table_name) .. 66 | [[ 67 | 68 | GROUP BY TABLE_NAME, PAGE_TYPE, INDEX_TYPE) tmp 69 | 70 | WHERE PAGE_TYPE = 'INDEX'; 71 | ]] 72 | 73 | local query_test = [[ 74 | select * from sbtest1 limit 5; 75 | ]] 76 | local result = con:query(query) 77 | if result == nil 78 | then 79 | print("Error! Can't query statistic from information schema") 80 | else 81 | print("INDEX_TYPE | PAGES | PCT_OF_BUFFER_POOL | PCT_OF_INDEX") 82 | part_lengths = {11, 7, 20, 13} -- lengths of headers 83 | local primary_index = format_array(result:fetch_row(), part_lengths) 84 | print(primary_index) 85 | 86 | secondary_index_row = result:fetch_row() 87 | if secondary_index_row ~= nil 88 | then 89 | local secondary_index = format_array(secondary_index_row, part_lengths) 90 | print(secondary_index) 91 | end 92 | end 93 | end 94 | 95 | 96 | -- This function just make a string from array where each element 97 | -- of array around with spaces to have length from part_lengths 98 | function format_array(arr, part_lengths) 99 | assert(#arr > 3, "Function format_array requires more then 3 array values") 100 | local string = "" 101 | for k, v in pairs(arr) 102 | do 103 | local bit = 0 104 | local val = tostring(v) 105 | while #val < part_lengths[k] 106 | do 107 | if bit == 0 108 | then 109 | val = ' ' .. val 110 | else 111 | val = val .. ' ' 112 | end 113 | bit = (bit + 1) % 2 114 | end 115 | string = string .. val 116 | if k ~= 4 117 | then 118 | string = string .. "|" 119 | end 120 | end 121 | return string 122 | end -------------------------------------------------------------------------------- /thread_groups.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2020-2022 Dmitrii Maximenko 2 | 3 | -- Use of this source code is governed by an MIT-style 4 | -- license that can be found in the LICENSE file or at 5 | -- https://opensource.org/licenses/MIT 6 | 7 | ffi = require("ffi") 8 | 9 | ffi.cdef[[ 10 | void sb_event_start(int thread_id); 11 | void sb_event_stop(int thread_id); 12 | bool sb_more_events(int thread_id); 13 | 14 | double sb_rand_uniform_double(void); 15 | static double sb_rand_exp(double lambda, double uniform_parameter) 16 | { 17 | return -lambda * log(1 - uniform_parameter); 18 | } 19 | 20 | typedef struct timeval { 21 | long tv_sec; 22 | long tv_usec; 23 | } timeval; 24 | 25 | int gettimeofday(struct timeval* t, void* tzp); 26 | int usleep(unsigned int usec); 27 | ]] 28 | 29 | gettimeofday_struct = ffi.new("struct timeval") 30 | local function gettimeofday() 31 | ffi.C.gettimeofday(gettimeofday_struct, nil) 32 | return tonumber(gettimeofday_struct.tv_sec) * 1000000 + tonumber(gettimeofday_struct.tv_usec) 33 | end 34 | 35 | -- ---------------------------------------------------------------------- 36 | -- This event loop can work with muliple groups of threads. 37 | -- For that purpose global variable 'thread_groups' should 38 | -- be defined in benchmark script.end 39 | -- thread_groups = {{id=i, event=func1, rate_controller=func2, rate=k, thread_amount=n}, {}, ...} 40 | -- id is any number or string. 41 | -- func1 is function as event(), func2 is functions witch controls sleeps period (optional), 42 | -- n is amount of threads in the group (im sum of groups threads_amount less then in --threads 43 | -- than all other threads go to the last group) 44 | -- rate is integer corresponding rate of the thread 45 | -- ---------------------------------------------------------------------- 46 | function thread_run(thread_id) 47 | while ffi.C.sb_more_events(thread_id) do 48 | ffi.C.sb_event_start(thread_id) 49 | 50 | local event_func = nil 51 | local rate_controller_func = nil 52 | local rate = 0 53 | local acc = 0 54 | for i = 1, #thread_groups 55 | do 56 | acc = acc + thread_groups[i].thread_amount 57 | if thread_id < acc 58 | then 59 | event_func = thread_groups[i].event 60 | rate_controller_func = thread_groups[i].rate_controller 61 | rate = thread_groups[i].rate 62 | break 63 | end 64 | 65 | -- if total amount of threads more then sum of amounts in groups 66 | -- all other threads rely to the last group 67 | if i == #thread_groups 68 | then 69 | event_func = thread_groups[i].event 70 | rate_controller_func = thread_groups[i].rate_controller 71 | rate = thread_groups[i].rate 72 | end 73 | end 74 | 75 | local success, ret 76 | repeat 77 | success, ret = pcall(event_func, thread_id) 78 | 79 | if not success then 80 | if type(ret) == "table" and 81 | ret.errcode == sysbench.error.RESTART_EVENT 82 | then 83 | if sysbench.hooks.before_restart_event then 84 | sysbench.hooks.before_restart_event(ret) 85 | end 86 | else 87 | error(ret, 2) -- propagate unknown errors 88 | end 89 | end 90 | until success 91 | 92 | -- Stop the benchmark if event() returns a value other than nil or false 93 | if ret then 94 | break 95 | end 96 | ffi.C.sb_event_stop(thread_id) 97 | 98 | -- It supposed that rete controller return execution flow after some pause 99 | if rate_controller_func ~= nil and rate > 0 100 | then 101 | pcall(rate_controller_func, rate) 102 | end 103 | 104 | 105 | end 106 | end 107 | 108 | 109 | 110 | -- This function controls rate of thread's events in that way: 111 | -- It stores the last time it was called and calculate random 112 | -- exponentially distributed interval to the next event (with median 1sec/rate). 113 | -- When it called again it measure time and if it passed less time then 'inteval' 114 | -- it sleeps for the rest part of interval. 115 | function default_rate_controller(rate) 116 | 117 | local Tcur = gettimeofday() 118 | local lambda = 1e6 / rate 119 | 120 | local uniform_parameter = ffi.C.sb_rand_uniform_double() 121 | local interval = -lambda * math.log(uniform_parameter) 122 | 123 | if Tnext == nil 124 | then 125 | Tnext = Tcur + interval 126 | else 127 | Tnext = Tnext + interval 128 | if Tnext > Tcur 129 | then 130 | ffi.C.usleep(Tnext - Tcur); 131 | end 132 | end 133 | end --------------------------------------------------------------------------------