Разделяй и властвуй

от автора


При работе с базой данных (в частности с 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 и дополнительные горутины (разве что на совсем диких объемах данных).

В коде на гитхаб я привожу рабочий пример данного подхода. В моих задачах используются еще другие пакеты, которые я вычистил из приведенного когда, но основная идея не должна от этого пострадать.

В общем, жду от заинтересованного сообщества конструктивной критики. Спасибо!

ссылка на оригинал статьи https://habr.com/ru/post/485056/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *