0

I'm new to golang and also to database optimizations.

I have simple app written on go and mysql database where send queries initialised through the web.

For receiving requests it take around 5s or little bit more? Is it possible somehow to optimize it?

Also if refreshing several times, then response could be already 50s and even more, exceptions with "invalid memory address or nil pointer dereference" or "Error 1040: Too many connections could appear".

How to avoid this and have all requests to be managiable in a efficient time frame?

This is table structure

 CREATE TABLE sportsmen (
    sp_no int(11) NOT NULL, 
    birth_date date NOT NULL, 
    first_name varchar(14) NOT NULL, 
    last_name varchar(16) NOT NULL, 
    gender enum('M','F') NOT NULL, 
    start_date date NOT NULL, 
    PRIMARY KEY (sp_no)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

CREATE TABLE points (
sp_no INT NOT NULL,
point INT NOT NULL,
date DATE NOT NULL
);

Number of records is around 300000 for sportsmen and 1 000 000 for their points.

this is function which is called on every request

var db *sql.DB

func init() {
    db, _ = sql.Open("mysql", "<connection>?charset=utf8")
    //checkErr(errcon)
    err := db.Ping()
    checkErr(err)
    yt := reflect.TypeOf(db).Kind()
    fmt.Printf("%T: %s\n", yt, yt)
}

func sportsmanPoints(w http.ResponseWriter, r *http.Request) {

    start := time.Now()

    sportsmen, err := db.Query("SELECT sp_no, first_name FROM sportsmen LIMIT ?,20", rand.Intn(100000))
    checkErr(err)

    for sportsmen.Next() {
        var spNo string
        var firstName string
        err = sportsmen.Scan(&spNo, &firstName)
        checkErr(err)
        spPoints, err := db.Query("SELECT max(point) FROM points WHERE sp_no =" + spNo)
        for spPoints.Next() {
            var spPoint int
            err = spPoints.Scan(&spPoint)
            checkErr(err)
            points.Data = ​append​(points.Data, Point{Name: firstName, Point: spPoint})
        }
    }

    data, err := json.Marshal(points.Data)
    if​ err != ​nil​ {
           log.Fatal(err)
     }

    fmt.Fprintln(w, ​string​(data))
    elapsed := time.Since(start)
    fmt.Println(​"Date:"​, time.Now(), ​"Response time:"​, elapsed)
    points.Data = ​nil
    data = ​nil
}

func​ ​checkErr​(err error) {
    if​ err != ​nil​ {
        panic​(err)
    }
}

func​ ​main​() {
    http.HandleFunc(​"/"​, sportsmanPoints)
    err := http.ListenAndServe(​":9090"​, ​nil​)
    if​ err != ​nil​ {
        log.Fatal(​"ListenAndServe: "​, err)
    }
}

Thank you.

Rick James
  • 135,179
  • 13
  • 127
  • 222
yart
  • 7,515
  • 12
  • 37
  • 37
  • 1
    This question is _far_ too broad. Start by learning how to use EXPLAIN to see how your queries are evaluated, play around with indices to optimize your queries. Learn how to combine nested queries into one. Learn how to tune performance of Go programs. – Volker Jun 05 '19 at 09:31
  • @Volker Thank you for comment. This is point, since it's I'm new to database optimisation and Go, this is where I'm asking the direction. EXPLAIN I have already started looking. – yart Jun 05 '19 at 09:38
  • 1
    Another thing that will help: Use a join, rather than multiple queries in a loop. – Jonathan Hall Jun 05 '19 at 10:41
  • @Flimzy, right, let me try it. – yart Jun 05 '19 at 11:16

2 Answers2

3

You connect to your database in your handler, every time a request is made to your server. This operation alone could take multiple seconds. Once the handler returns, you just throw away that connection (you don't even close that, so that may be idling for some time before getting closed, hogging db server resources as connections are most likely limited on the server). Don't do that.

Connect to your database once, on app startup, and use this connection in your handler. The database will keep idle connections open, ready to be reused immediately should you need it in another request.

Move your db variable outside, e.g. to a package-level variable, connect to your database and initialzie this db variable once, e.g. in your main() or in a package init() function, and just use it in your handler.

sql.Open() documents that:

The returned DB is safe for concurrent use by multiple goroutines and maintains its own pool of idle connections. Thus, the Open function should be called just once. It is rarely necessary to close a DB.

See similar question: mgo - query performance seems consistently slow (500-650ms)

icza
  • 389,944
  • 63
  • 907
  • 827
  • Thank you for reply. I was thinking about database opening also and found also on this http://go-database-sql.org/accessing.html page, Don’t Open() and Close() databases frequently. Instead, create one sql.DB object for each distinct datastore you need to access, and keep it until the program is done accessing that datastore. Pass it around as needed. I made changes in code and in the question by adding global variable and init method, unfortunately it's the same result. – yart Jun 05 '19 at 11:12
  • when I say the same I mean, around 5 second on first request and if you want to have 10 in the same time then around 50+ seconds. Moreover it's returning different length of results, sometimes 20 records, sometimes 7 and sometimes 1 record in response. – yart Jun 05 '19 at 11:15
  • 1
    @yart Do you have database indices in place for your queries? – icza Jun 05 '19 at 11:36
  • Are you speaking about non-clustered indexes? Could you provide sample? – yart Jun 05 '19 at 19:40
  • @yart See [MySQL - INDEXES](https://www.tutorialspoint.com/mysql/mysql-indexes.htm) and [How MySQL Uses Indexes](https://dev.mysql.com/doc/refman/8.0/en/mysql-indexes.html). – icza Jun 06 '19 at 08:50
  • Combination of fixing keys and the way of opening database connection returned around 20ms-70ms timing. Thank you. Do you know why still sometimes it's 20ms and sometimes 70ms? – yart Jun 10 '19 at 10:26
  • @yart That fluctuation might be the result of many things, e.g. if a new DB connection is made in the background, that obviously takes more time than if an existing one is just reused. Also the database server might respond to queries made in the past faster if they are cached. You should not worry about that if even the slowest is just 70 ms. – icza Jun 10 '19 at 13:24
  • thank again. One more clarification. Despite that I have globally defined connection to the database I still could have "too much open connections" if I will make let's 1000 requests in the same time or global definition of the db variables somehow also controls it? – yart Jun 10 '19 at 13:39
  • @yart The `sql.DB` manages an internal connection pool which by default does not limit open connections. Use [`DB.SetMaxOpenConns()`](https://golang.org/pkg/database/sql/#DB.SetMaxOpenConns) to limit the max open connections. – icza Jun 10 '19 at 13:57
0
SELECT sp_no, first_name FROM sportsmen LIMIT ?,20", rand.Intn(100000)

Terrible performance. And poor results.

  • It will only pick from the first 1/3 of the table (cf 100000 vs 300000).

  • It will pick the same 20 or overlapping 20 occasionally.

  • It must skip over up to 100000 rows before finding the 20. (This is the performance problem.)

I have multiple ways to improve on all three of those issues: http://mysql.rjweb.org/doc.php/random

And by speeding up the query, the "too many connections" is likely to go away.

Rick James
  • 135,179
  • 13
  • 127
  • 222
  • thank you for comment. This is where I'm learning. Since after opening your link it's still not fully clear for me, are you referring to particular case which you are advising? Would you please write sample based on your recommendations? – yart Jun 05 '19 at 22:16
  • @yart - First, which case applies? And please provide `SHOW CREATE TABLE sportsmen` – Rick James Jun 05 '19 at 22:46
  • Table | Create Table sportsmen | CREATE TABLE `sportsmen` ( `sp_no` int(11) NOT NULL, `birth_date` date NOT NULL, `first_name` varchar(14) NOT NULL, `last_name` varchar(16) NOT NULL, `gender` enum('M','F') NOT NULL, `start_date` date NOT NULL, PRIMARY KEY (`sp_no`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 | – yart Jun 06 '19 at 01:05
  • @yart - `sp_no` -- not `AUTO_INCREMENT`? What are the min and max values? `SELECT MIN(sp_no), MAX(sp_no) FROM sportsmen` – Rick James Jun 06 '19 at 01:32
  • @yart - I lean toward "Case: AUTO_INCREMENT with gaps, 1 or more rows returned", replacing: `id`->`sp_no`, `RandTest`->`sportsmen`, `10`->`20`, `50`->`100`. – Rick James Jun 06 '19 at 01:39