Русский
Русский
English
Статистика
Реклама

Post

Ускоряем загрузку больших объёмов в PostgreSQL, используя COPY from STDIN binary

06.01.2021 14:15:34 | Автор: admin

Предисловие

Я изучаю PostgreSQL дома и очень люблю обрабатывать большое количество данных. Пишу на ЯП C/C++ на Qt фреймворке. К сожалению Qt драйвер для постреса не поддерживает функционал, необходимый для быстрой загрузки. Поэтому я написал свою библиотеку на С++ для этого, а теперь хочу с Вами поделиться этим прекрасным методом добавления и самой библиотекой.

Привет, $username !

Сегодня пойдёт речь о быстрой загрузке данных в СУБД PostgreSQL ( далее `постик` ). Делать мы это будем через механизм COPY с передачей данных по сети в бинарном формате.

Первым делом рассмотрим плюсы данной методики добавления:

  • Очень большая скорость добавления

    Всё обуславливается тем, что мы на минимум снимаем потребность в обработке данных ( разные преобразования ), постику остаётся только проверять, правильный ли мы используем формат.

  • Не теряем данные, в отличии от текстового формата.

    Например, как это может произойти с double числом. Нам не нужно будет в этом методе выяснять, сколько знаков до и после запятой. Данные передаются `как есть`.

В данном посте я не буду раскрывать все подробности, которые описаны в документации. Мы просто напишем лёгкий метод добавления, т.е. без специфик и прочего. Все функции, которые будут вызываться в коде это функции из библиотеки libpq-fe.h. Так-же весь код будет писаться на С/С++.

Алгоритм создания бинарного буфера

Структура буфера:

[шапка начала буфера]

{строка данных}

{строка данных}

{строка данных}

. . .

{строка данных}

[шапка конца буфера]

Шапка начала буфера

Шапка начала буфера состоит из следующей последовательности байт:

  • Сигнатура COPY-буфера

'P','G','C','O','P','Y','\n','\377','\r','\n','\0'
  • Поле флагов

'\0','\0','\0','\0'

В случае, если мы включаем в данные OID выставляем 16-й бит в 1

  • Длина области расширения заголовка

'\0','\0','\0','\0'

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

  • Расширение заголовка

    Поскольку в текущих версиях этого расширения нет ( на данный момент 13.1 последняя версия ), то ничего и не пишем.

Строка данных

Строка данных состоит из:

  • Длинна записи

Это int16_t число, указывающее, сколько столбцов будет добавляется в текущем столбце. На данное время это число постоянно одно и то же, в будущем, возможно, будут изменения.

  • После этого следует указанное количество данных о столбцах:

    1) Длинна данных

    Указываем, сколько байт занимают данные, которые нужно добавить в текущий столбец

    2) Непосредственно сами данные

Хотелось бы тут заметить, что разные типы данных могут добавляются совсем по другому:

Например массивы.

Чтобы добавить массив int64_t нужно вставлять не просто данные, а их сигнатуру. ( там идёт своя шапка данных и описание массива ). К сожалению, описание добавления для каждого типа не описано в док-ции постреса. Поэтому, чтобы узнать сигнатуру массива надо или смотреть в исходники или создавать дамп через COPY TOи уже оттуда смотреть, что и как лежит.

Шапка конца буфера

0xff, 0xff

Для дополнительной синхронизации передачи буфер необходимо закрыть. Таким образом, когда постик будет просматривать новую длину строки данных увидит, что достигнут конец буфера (ну данные ведь не могут быть длинной -1).

Алгоритм добавления данных

  • Для того, чтобы подключиться к серверу создадим подключение

string conninfo =   "host=127.0.0.1 port=5432 dbname=postgres user=postgres password=postgres connect_timeout=10";PGconn *conn = PQconnectdb(conninfo.c_str());// conninfo - это срока параметрами подключения ( connect_timeout измеряется в секундах )
  • Подготовим COPY-запрос

pg_result *res = PQexec(conn, cquery);

в качестве cquery у нас выступает COPY запрос. В моём примере это COPY testtable5 ( col1, col2, col3, col4 ) FROM STDIN (format binary);

Добавим буфер

PQputCopyData(conn, buf, currentSize);

, где buf указатель на буфер, currentSize длинна буфера в байтах.

Очень важно то, что буфер можно передавать частями. Это очень удобно. Я лично использую буферы по 2-128 Мб.

  • Скажем серверу, что это конец данных

PQputCopyEnd(conn, NULL);

Очень важно!

Все данные должны передаваться в сетевом формате.

Что это значит? Например, int16_t tmp = 2; На самом деле в оперативке данные будут лежать так: 0x02, 0x00 А не так как мы привыкли 0x00, 0x02. Это связано с архитектурой процессора. Процессоры архитектуры SPARC хранят данные уже в сетевом формате. Поэтому, если у вас не SPARC-архитектура, нужно все байты вставлять в буфер задом на перёд ( за исключением строк )

Немного графиков

Я сделал вторую программу для добавления в БД строк данных.
Написано это было на Qt:

db.open();QSqlQuery query(db);query.prepare("insert into testtable5 ( col1, col2, col3, col4 ) values (?,?,?,?);");for(int i=0; i<20000000; i++){    query.addBindValue("column1");    query.addBindValue(double(12983712987.4383453947384734853872837));    query.addBindValue(int(12345678));    query.addBindValue(float(123.4567));    query.exec();}

Нижеприведённые графики будут показывать время добавления ( в мс ) следующих 10.000 данных - по оси Y, кол-во добавленных данных - по оси X.

Сравнительный график скорости работы COPY и INSERT запросов.

Красным - INSERT-вставка в постоянную таблицу.

Зелёный - INSERT-вставка во временную таблицу.

Синий- COPY-вставка в постоянную таблицу.

Жёлтый - COPY-вставка во временную таблицу.

Сравнительный график скорости работы INSERT запросов.

Жёлтый - INSERT-вставка в постоянную таблицу.

Синий- INSERT-вставка во временную таблицу.

Сравнительный график скорости работы COPY запросов.

Синий- COPY-вставка в постоянную таблицу.

Жёлтый - COPY-вставка во временную таблицу.

Немного расчётов

Давайте посмотрим на графики и попробуем найти средние показатели добавления.

Вот что у меня получается:

Чтобы добавить порцию из 10.000 данных ( строк ), мне требуется:

12.620 мс на добавление в постоянную таблицу при помощи INSERT

12.050 мс на добавление во временную таблицу при помощи INSERT

150 мс на добавление в постоянную таблицу при помощи COPY

120 мс на добавление во временную таблицу при помощи COPY

Тут хотелось бы остановиться сразу и сказать... Что я не смог замерить время коммита запроса COPY. Думаю, оно не сильно будет играть роль.

Исходный код

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

Ссылка на проект на GitHub

Ссылка на документацию по использованию COPY

P.S.: это мой первый пост, прошу меня простить за кривизну моих рук.

Хотелось бы услышать хороших комментариев и конструктивной критики.

Подробнее..

Основы Flutter для начинающих (Часть VI)

05.06.2021 14:06:41 | Автор: admin

Когда вы создаете различные формы (например: регистрации или входа) на Flutter, вы не заморачиваетесь с кастомизацией компонентов, потому что вы можете изменить любое поле формы под свой стиль.

Помимо кастомизации, Flutter предоставляет возможность обработки ошибок и валидации полей формы.

И сегодня мы постараемся разобраться с этой темой на небольшом примере.

Ну что ж, погнали!

Наш план
  • Часть 1- введение в разработку, первое приложение, понятие состояния;

  • Часть 2- файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3- BottomNavigationBar и Navigator;

  • Часть 4- MVC. Мы будем использовать именно этот паттерн, как один из самых простых;

  • Часть 5 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6 (текущая статья) - работа с формами, текстовые поля и создание поста.

  • Часть 7 - работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 8 - создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 9 - немного о тестировании;

Создание формы: добавление поста

Для начала добавим на нашу страницу HomePage кнопку по которой мы будем добавлять новый пост:

@overrideWidget build(BuildContext context) {  return Scaffold(    appBar: AppBar(      title: Text("Post List Page"),    ),    body: _buildContent(),    // в первой части мы уже рассматривали FloatingActionButton    floatingActionButton: FloatingActionButton(      child: Icon(Icons.add),      onPressed: () {      },    ),  );}

Далее создадим новую страницу в файле post_add_page.dart:

import 'package:flutter/material.dart';class PostDetailPage extends StatefulWidget {  @override  _PostDetailPageState createState() => _PostDetailPageState();}class _PostDetailPageState extends State<PostDetailPage> {    // TextEditingController'ы позволят нам получить текст из полей формы  final TextEditingController titleController = TextEditingController();  final TextEditingController contentController = TextEditingController();    // _formKey пригодится нам для валидации  final _formKey = GlobalKey<FormState>();    @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post Add Page"),        actions: [          // пункт меню в AppBar          IconButton(            icon: Icon(Icons.check),            onPressed: () {              // сначала запускаем валидацию формы              if (_formKey.currentState!.validate()) {                // здесь мы будем делать запроc на сервер              }            },          )        ],      ),      body: Padding(        padding: EdgeInsets.all(15),        child: _buildContent(),      ),    );  }  Widget _buildContent() {    // построение формы    return Form(      key: _formKey,      // у нас будет два поля      child: Column(        children: [          // поля для ввода заголовка          TextFormField(            // указываем для поля границу,            // иконку и подсказку (hint)            decoration: InputDecoration(                border: OutlineInputBorder(),                prefixIcon: Icon(Icons.face),                hintText: "Заголовок"            ),            // не забываем указать TextEditingController            controller: titleController,            // параметр validator - функция которая,            // должна возвращать null при успешной проверки            // или строку при неудачной            validator: (value) {              // здесь мы для наглядности добавили 2 проверки              if (value == null || value.isEmpty) {                return "Заголовок пустой";              }              if (value.length < 3) {                return "Заголовок должен быть не короче 3 символов";              }              return null;            },          ),          // небольшой отступ между полями          SizedBox(height: 10),          // Expanded означает, что мы должны          // расширить наше поле на все доступное пространство          Expanded(            child: TextFormField(              // maxLines: null и expands: true               // указаны для расширения поля на все доступное пространство              maxLines: null,              expands: true,              textAlignVertical: TextAlignVertical.top,              decoration: InputDecoration(                  border: OutlineInputBorder(),                  hintText: "Содержание",              ),              // не забываем указать TextEditingController              controller: contentController,              // также добавляем проверку поля              validator: (value) {                if (value == null || value.isEmpty) {                  return "Содержание пустое";                }                return null;              },            ),          )        ],      ),    );  }}

Не забудьте добавить переход на страницу формы:

floatingActionButton: FloatingActionButton(   child: Icon(Icons.add),   onPressed: () {      Navigator.push(context, MaterialPageRoute(         builder: (context) => PostDetailPage()      ));   },),

Запускаем и нажимаем на кнопку:

Вуаля! Форма работает.

Небольшая заметка

У новичков могут возникнуть проблемы даже с готовым кодом. И это не издевательство, такое бывает.

Поэтому для 100%-ной работы коды постарайтесь использовать схожие версии Flutter и Dart с моими:

  • Flutter 2.0.6

  • Dart SDK version: 2.12.3

Также в комментах я обратил внимание на null safety. Это очень важно, я позабыл об этом и это мой косяк.

Я уже добавил в приложение поддержку null safety. Вы наверно обратили внимание на восклицательный знак:

// ! указывает на то, что мы 100% уверены// что currentState не содержит null значение_formKey.currentState!.validate()

О null safety и о её поддержи в Dart можно сделать целый цикл статей, а возможно и написать целую книгу.

Мы задерживаться не будем и переходим к созданию POST запроса.

POST запрос для добавления данных на сервер

POST, как уже было отмечено, является одним из HTTP методов и служит для добавления новых данных на сервер.

Для начала добавим модель для нашего результата и изменим немного класс Post:

class Post {  // все поля являются private  // это сделано для инкапсуляции данных  final int? _userId;  final int? _id;  final String? _title;  final String? _body;  // создаем getters для наших полей  // дабы только мы могли читать их  int? get userId => _userId;  int? get id => _id;  String? get title => _title;  String? get body => _body;  // добавим новый конструктор для поста  Post(this._userId, this._id, this._title, this._body);  // toJson() превращает Post в строку JSON  String toJson() {    return json.encode({      "title": _title,      "content": _body    });  }  // Dart позволяет создавать конструкторы с разными именами  // В данном случае Post.fromJson(json) - это конструктор  // здесь мы принимаем объект поста и получаем его поля  // обратите внимание, что dynamic переменная  // может иметь разные типы: String, int, double и т.д.  Post.fromJson(Map<String, dynamic> json) :    this._userId = json["userId"],    this._id = json["id"],    this._title = json["title"],    this._body = json["body"];}// у нас будут только два состоянияabstract class PostAdd {}// успешное добавлениеclass PostAddSuccess extends PostAdd {}// ошибкаclass PostAddFailure extends PostAdd {}

Затем создадим новый метод в нашем Repository:

// добавление поста на серверFuture<PostAdd> addPost(Post post) async {  final url = Uri.parse("$SERVER/posts");  // делаем POST запрос, в качестве тела  // указываем JSON строку нового поста  final response = await http.post(url, body: post.toJson());  // если пост был успешно добавлен  if (response.statusCode == 201) {    // говорим, что все ок    return PostAddSuccess();  } else {    // иначе ошибка    return PostAddFailure();  }}

Далее добавим немного кода в PostController:

// добавление поста// функция addPost будет принимать callback,// через который мы будет получать результатvoid addPost(Post post, void Function(PostAdd) callback) async {  try {    final result = await repo.addPost(post);    // сервер вернул результат    callback(result);  } catch (error) {    // произошла ошибка    callback(PostAddFailure());  }}

Ну что ж пора нам вернуться к нашему представлению PostAddPage:

class PostDetailPage extends StatefulWidget {  @override  _PostDetailPageState createState() => _PostDetailPageState();}// не забываем поменять на StateMVCclass _PostDetailPageState extends StateMVC {  // _controller может быть null  PostController? _controller;  // получаем PostController  _PostDetailPageState() : super(PostController()) {    _controller = controller as PostController;  }  // TextEditingController'ы позволят нам получить текст из полей формы  final TextEditingController titleController = TextEditingController();  final TextEditingController contentController = TextEditingController();  // _formKey нужен для валидации формы  final _formKey = GlobalKey<FormState>();  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post Add Page"),        actions: [          // пункт меню в AppBar          IconButton(            icon: Icon(Icons.check),            onPressed: () {              // сначала запускаем валидацию формы              if (_formKey.currentState!.validate()) {                // создаем пост                // получаем текст через TextEditingController'ы                final post = Post(                  -1, -1, titleController.text, contentController.text                );                // добавляем пост                _controller!.addPost(post, (status) {                  if (status is PostAddSuccess) {                    // если все успешно то возвращаемя                    // на предыдущую страницу и возвращаем                    // результат                    Navigator.pop(context, status);                  } else {                    // в противном случае сообщаем об ошибке                    // SnackBar - всплывающее сообщение                    ScaffoldMessenger.of(context).showSnackBar(                      SnackBar(content: Text("Произошла ошибка при добавлении поста"))                    );                  }                });              }            },          )        ],      ),      body: Padding(        padding: EdgeInsets.all(15),        child: _buildContent(),      ),    );  }  Widget _buildContent() {    // построение формы    return Form(      key: _formKey,      // у нас будет два поля      child: Column(        children: [          // поля для ввода заголовка          TextFormField(            // указываем для поля границу,            // иконку и подсказку (hint)            decoration: InputDecoration(                border: OutlineInputBorder(),                prefixIcon: Icon(Icons.face),                hintText: "Заголовок"            ),            // указываем TextEditingController            controller: titleController,            // параметр validator - функция которая,            // должна возвращать null при успешной проверки            // и строку при неудачной            validator: (value) {              // здесь мы для наглядности добавили 2 проверки              if (value == null || value.isEmpty) {                return "Заголовок пустой";              }              if (value.length < 3) {                return "Заголовок должен быть не короче 3 символов";              }              return null;            },          ),          // небольшой отступ между полями          SizedBox(height: 10),          // Expanded означает, что мы должны          // расширить наше поле на все доступное пространство          Expanded(            child: TextFormField(              // maxLines: null и expands: true              // указаны для расширения поля              maxLines: null,              expands: true,              textAlignVertical: TextAlignVertical.top,              decoration: InputDecoration(                  border: OutlineInputBorder(),                  hintText: "Содержание",              ),              // указываем TextEditingController              controller: contentController,              // также добавляем проверку поля              validator: (value) {                if (value == null || value.isEmpty) {                  return "Содержание пустое";                }                return null;              },            ),          )        ],      ),    );  }}

Логика работы следующая:

  1. мы нажаем добавить новый пост

  2. открывается окно с формой, вводим данные

  3. если все ок, то возвращаемся на предыдущую страницу и сообщаем об этом иначе выводим сообщение об ошибке.

Заключительный момент, добавим обработку результата в PostListPage:

floatingActionButton: FloatingActionButton(  child: Icon(Icons.add),  onPressed: () {    // then возвращает объект Future    // на который мы подписываемся и ждем результата    Navigator.push(context, MaterialPageRoute(      builder: (context) => PostDetailPage()    )).then((value) {      if (value is PostAddSuccess) {        // SnackBar - всплывающее сообщение        ScaffoldMessenger.of(context).showSnackBar(         SnackBar(content: Text("Пост был успешно добавлен"))        );      }    });  },),

Теперь тестируем:

К сожалению JSONPlaceholder на самом деле не добавляет пост и поэтому мы не сможем его увидеть среди прочих постов.

Заключение

Я надеюсь, что убедил вас в том, что работа с формами на Flutter очень проста и не требует почти никаких усилий.

Большая часть кода - это создание POST запроса на сервер и обработка ошибок.

Полезные ссылки

Всем хорошего кода)

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru