
При работе с базой данных (в частности с PostgreSQL) у меня появилась идея выбирать данные из таблицы параллельно (используя ЯП Go). И я задался вопросом «возможно ли сканировать строки выборки в отдельных гоурутинах».
Как оказалось, func (*Rows) Scan нельзя вызывать одновременно в гоурутинах. Исходя из этого ограничения, я решил выполнять параллельно со сканированием строк другие процессы, в частности, подготовку результирующих данных.
Т.к. Scan складывает данные по указателям, я решил сделать два среза (позже я поясню почему именно два), между которыми я буду переключать Scan, в то время как остальные гоурутины будут разбираться с уже выбранными данными.
Изначально мне нужно знать количество колонок выборки:
columns, err = rows.Columns() count := len(columns)
Далее я создаю по два среза со значениями и с указателями на эти значения (куда и буду складывать данные во время сканирования строк):
values := make([]interface{}, count) valuesPtrs := make([]interface{}, count) values_ := make([]interface{}, count) valuesPtrs_ := make([]interface{}, count) for i := range columns { valuesPtrs[i] = &values;[i] valuesPtrs_[i] = &values;_[i] }
В данном примере я буду складывать результат выборки в map[string]string, где ключами будут имена колонок. Можно использовать конкретную структуру с указанием типов, но т.к. цель данной публикации узнать у хабрасообщества насколько жизнеспособный предлагаемый подход, остановимся на выборке в map.
Далее я отделяю две гоурутины, одна из которых будет формировать результирующий map:
func getData(deleteNullValues bool, check, finish chan bool, dbData chan interface{}, columns []string, data *[]map[string]string) { lnc := len(columns) for <-check { row := make(map[string]string) for i := 0; i < lnc; i++ { el := <-dbData b, ok := el.([]byte) if ok { row[columns[i]] = string(b) } else { if el == nil { if deleteNullValues == false { row[columns[i]] = "" } } else { row[columns[i]] = fmt.Sprint(el) } } } *data = append(*data, row) } finish <- true }
А вторая будет переключаться между двумя срезами со значениями, сформированными Scan и отправлять их в канал для предыдущей гоурутины (которая формирует результат):
func transferData(values, values_ []interface{}, dbData chan interface{}, swtch, working, check chan bool) { for <-working { check <- true switch <-swtch { case false: for _, v := range values { dbData <- v } default: for _, v := range values_ { dbData <- v } } } }
Основной процесс будет заниматься переключением между срезами указателей и выбирать данные:
for rows.Next() { switch chnl { case false: if err = rows.Scan(valuesPtrs...); err != nil { fmt.Printf("rows.Scan: %s\n%s\n%#v\n", err, query, args) return nil, nil, err } default: if err = rows.Scan(valuesPtrs_...); err != nil { fmt.Printf("rows.Scan: %s\n%s\n%#v\n", err, query, args) return nil, nil, err } } working <- true swtch <- chnl chnl = !chnl }
В базе данных я сформировал таблицу с 32-я колонками и добавил в нее 100k строк.
В результате теста (при выборке данных 50 раз) у меня получились следующие данные:
Time spent: 1m8.022277124s — выборка результата с использованием одного среза
Time spent: 1m7.806109441s — выборка результата с использованием двух срезов
При увеличении количества итераций до 100:
Time spent: 2m15.973344023s — выборка результата с использованием одного среза
Time spent: 2m15.057413845s — выборка результата с использованием двух срезов
Разница увеличивается при увеличении объема данных и увеличении колонок в таблице.
Однако обратный результат наблюдался при уменьшении объема данных или при уменьшении кол-ва колонок таблицы, что, в принципе, понятно, т.к. накладные расходы подготовительных шагов и отделения гоурутин «съедают» драгоценное время и результат нивелируется.
Что касается двух срезов и двух гоурутин: я проводил тесты с большим количеством срезов, но время выборки увеличивалось, т.к., очевидно, функции getData и transferData обрабатывают данные быстрее, чем происходит сканирование значений из базы. Поэтому, даже при наличии большего количества ядер, нет смысла добавлять новые срезы для Scan и дополнительные горутины (разве что на совсем диких объемах данных).
В коде на гитхаб я привожу рабочий пример данного подхода. В моих задачах используются еще другие пакеты, которые я вычистил из приведенного когда, но основная идея не должна от этого пострадать.
В общем, жду от заинтересованного сообщества конструктивной критики. Спасибо!
