简体   繁体   中英

Investigating slow simple queries in JDBC and MySQL

PreparedStatement.executeQuery() is taking ~20x longer to execute than if it were run directly via the shell. I've logged with timers to determine that this method is the culprit.

The query and some DB info ( ignoring the Java issue for the moment ):

mysql> SELECT username from users where user_id = 1; // lightning fast

Running that same query 1,000 times via mysqlslap is also lightning fast.

mysqlslap --create-schema=mydb --user=root -p --query="select username from phpbb_users where user_id = 1" --number-of-queries=1000 --concurrency=1

Benchmark
        Average number of seconds to run all queries: 0.051 seconds
        Minimum number of seconds to run all queries: 0.051 seconds
        Maximum number of seconds to run all queries: 0.051 seconds
        Number of clients running queries: 1
        Average number of queries per client: 1000

The Problem: Performing the same query in JDBC slows things significantly. In a for loop calling the below queryUsername() 1,000 times (this is called in the Main method, which isn't shown here) takes around 872ms. That's ~17x slower. I've tracked down the heavy usage by placing timers in various spots (omitted some for brevity). The primary suspect is stmt.executeQuery() which took 776ms of the 872ms runtime.

public static String queryUsername() {
        String username = "";
        // DBCore.getConnection() returns HikariDataSource.getConnection() implementation exactly as per https://www.baeldung.com/hikaricp
        try (Connection connection = DBCore.getConnection(); 
            PreparedStatement stmt = connection.prepareStatement("SELECT username from phpbb_users where user_id = ?");) {
            stmt.setInt(1, 1);  // just looking for user_id 1 for now
            // Google timer used to measure how long executeQuery() is taking
            // Another Timer is used outside of this method call to see how long
            // total execution takes. 
            // Approximately 1 second in for loop calling this method 1000 times
            Stopwatch s = Stopwatch.createStarted(); 
            try (ResultSet rs = stmt.executeQuery();) {
                s.stop(); // stopping the timer after executeQuery() has been called
                timeElapsed  += s.elapsed(TimeUnit.MICROSECONDS);
                while (rs.next())
                {
                    username = rs.getString("username"); // the query returns 1 record
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        
        return username;
    }

Additional context and things tried :

解释声明

在此处输入图像描述

  • SHOW OPEN TABLES has several tables open, but all have In_use=0 and Name_locked=0.
  • SHOW FULL PROCESSLIST looks healthy.
  • user_id is an indexed primary key
  • The Server is an Upcloud $5/month 1-Core, 1GB RAM running Ubuntu 20.04.1 LTS (GNU/Linux 5.4.0-66-generic x86_64). Mysql Ver 8.0.23-0ubuntu0.20.04.1 for Linux on x86_64 ((Ubuntu))
  • JDBC Driver is mysql-connector-java_8.0.23.jar, which was obtained from mysql-connector-java_8.0.23-1ubuntu20.04_all via https://dev.mysql.com/downloads/connector/j/

服务器 HTOP

Don't reconnect each time. Open the connection at the start; reuse it until the web page (or program) is finished.

Chances are that you are comparing different realities.

When running mysqlslap you are most likely using Unix Domain Sockets in the communication between the tool and MySQL server. Try changing that to TCP and you should observe an immediate performance drop. Connector/J, on the other hand, creates TCP based connections by default (Unix Domain Sockets can be used but only by using a third party library).

Also, in mysqlslap you are running a simple query directly, which are handled by a COM_QUERY protocol command. In the Java sample you are preparing the query first and then executing it. Depending on how Connector/J is configured this may result in a single COM_QUERY protocol command or a pair of commands, namely, COM_STMT_PREPARE and COM_STMT_EXECUTE. Connector/J is also affected by how its statement caches are configured (and/or the CP ones). However, you are only measuring the executeQuery part so, theoretically, Connector/J could be being favored here.

Finally, unless you actually come up with a use case where you guarantee that both executions are effectively doing the same work under the same circumstances, you can compare results and point out differences, but you can't take any conclusions from it. For example, it's not that hard to introduce caches and make those simple iterations even completely skip communicating to the server... that would make things extremely fast.

move borrowing connection and Stopwatch related code out of method. then measure as:

 Stopwatch s = Stopwatch.createStarted();
 try (Connection con = ....)  {
     for (int i=0; i < 1000; i++)  {
            queryUsername(   con   );
      }
 }
 s.stop();
 print s.elapsed(TimeUnit.MICROSECONDS);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM