SqlAlchemyのコネクションプーリングの効果比較

## SqlAlchemyのコネクションプーリングの効果比較
コネクションプーリングのあり・なしを比較してみた。
それぞれ、 `psycopg2` と `pg8000` で確認してした。

結果は、コネクションプーリングなしが 280～310倍遅いということになったが、それ以上に今回の測定では `pg8000` が若干早いという結果になった。

### 環境
* Ubuntu 18.04 LTS ... WSL1 (Windows 10)
  * libpq ... 13.2
* Python 3.8
  * SQLAlchemy 1.4.6
  * pg8000 1.19.1(Pure Python)
  * psycopg2 2.8.6 (Nativeライブラリを使用)

### 測定方法
測定方法は、参考1のやり方と合わせ1多重で1000回の接続・切断を行う。
ただし、コードの書き方は厳密に合わせはしない。なるべく今風の書き方をする。

なお、SqlAlchemyではデフォルトで `QueuePool` というものを使う。
コネクションプーリングを明示的に使わないようにするには `NullPool` を使う。

趣味の範囲なので、複数測った平均だとかはとらず、一発取り。1000回の試行の合計なので、それだけでバラつきは減る。プログラムの調整で複数回は実行するがバラつきはおよそ±3秒の範囲。

### 結果(概要)
参考にしたサイトだと 270 倍だったが、同じような水準になった。
なお、PCが貧弱で、かつ WSL1 を使っているので遅い面はあるので、相対値のみ参考にしてください。
(注: 1000で割った数が1コネクションあたりの時間になります。)

|               | pg8000               | psycopg2 |
|---------------|---------------------:|---------:|
| **QueuePool** |   0.078 [s/1000conn] |   0.094 [s/1000conn] |
| **NullPool**  | 21.747s [s/1000conn] |  28.930 [s/1000conn] |
| Ratio         |   279倍 |   307倍  |

#### 検証結果から言えること

* コネクションにかかるコストは相対的に大きい  
  1コネクションでみると一番遅いパターンでも 0.028 秒なので体感して遅い訳ではない
* pg8000(Pure Python) より、psycopg2(ネイティブクライアント使用)のが若干遅い

#### 検証結果に対する補足

* psycopg2 の方が遅いのは、この計測の場合 Python → C の呼び出しに関わるオーバヘッド(メモリ確保やデータ変換などの橋渡し)の影響が大きく出るのではないかと思われる
* 常駐プログラムでない場合は NullPool相当となる
  * その都度処理が終わるなら当然コネクションの使いまわしができないから

### 検証コード
共通化することはできるのだが、まぁいいや。

#### pg8000 + NullPool
```python
# -*- coding: utf-8 -*-
"""Example of postgresql+pg8000."""
import os
import time

import sqlalchemy


def main(engine):
    """Run main."""
    t_0 = time.time()
    for _ in range(1000):
        with engine.connect():
            pass
    t_1 = time.time()
    print(f'dt = {(t_1 - t_0):.3f}s')


if __name__ == '__main__':
    engine_pg8000 = sqlalchemy.create_engine(
        sqlalchemy.engine.URL.create(
            'postgresql+pg8000',
            host=os.environ.get('PGHOST'),
            port=os.environ.get('PGPORT'),
            database=os.environ.get('PGDATABASE'),
            username=os.environ.get('PGUSER'),
            password=os.environ.get('PGPASSWORD')
        ),
        poolclass=sqlalchemy.pool.NullPool
    )
    main(engine_pg8000)
    engine_pg8000.dispose()

# EOF
```

#### pg8000 + QueuePool(default)
```python
# -*- coding: utf-8 -*-
"""Example of postgresql+pg8000."""
import os
import time

import sqlalchemy


def main(engine):
    """Run main."""
    t_0 = time.time()
    for _ in range(1000):
        with engine.connect():
            pass
    t_1 = time.time()
    print(f'dt = {(t_1 - t_0):.3f}s')


if __name__ == '__main__':
    engine_pg8000 = sqlalchemy.create_engine(
        sqlalchemy.engine.URL.create(
            'postgresql+pg8000',
            host=os.environ.get('PGHOST'),
            port=os.environ.get('PGPORT'),
            database=os.environ.get('PGDATABASE'),
            username=os.environ.get('PGUSER'),
            password=os.environ.get('PGPASSWORD')
        ),
        pool_size=1,
        max_overflow=1
    )
    main(engine_pg8000)
    engine_pg8000.dispose()

# EOF
```

#### psycopg2 + NullPool
```python
# -*- coding: utf-8 -*-
"""Example of postgresql+psycopg2."""
import os
import time

import sqlalchemy


def main(engine):
    """Run main."""
    t_0 = time.time()
    for _ in range(1000):
        with engine.connect():
            pass
    t_1 = time.time()
    print(f'dt = {(t_1 - t_0):.3f}s')


if __name__ == '__main__':
    engine_psycopg2 = sqlalchemy.create_engine(
        sqlalchemy.engine.URL.create(
            'postgresql+psycopg2',
            host=os.environ.get('PGHOST'),
            port=os.environ.get('PGPORT'),
            database=os.environ.get('PGDATABASE'),
            username=os.environ.get('PGUSER'),
            password=os.environ.get('PGPASSWORD')
        ),
        poolclass=sqlalchemy.pool.NullPool
    )
    main(engine_psycopg2)
    engine_psycopg2.dispose()

# EOF
```

#### psycopg2 + QueuePool(default)
```python
# -*- coding: utf-8 -*-
"""Example of postgresql+psycopg2."""
import os
import time

import sqlalchemy


def main(engine):
    """Run main."""
    t_0 = time.time()
    for _ in range(1000):
        with engine.connect():
            pass
    t_1 = time.time()
    print(f'dt = {(t_1 - t_0):.3f}s')


if __name__ == '__main__':
    engine_psycopg2 = sqlalchemy.create_engine(
        sqlalchemy.engine.URL.create(
            'postgresql+psycopg2',
            host=os.environ.get('PGHOST'),
            port=os.environ.get('PGPORT'),
            database=os.environ.get('PGDATABASE'),
            username=os.environ.get('PGUSER'),
            password=os.environ.get('PGPASSWORD')
        ),
        pool_size=1,
        max_overflow=1
    )
    main(engine_psycopg2)
    engine_psycopg2.dispose()

# EOF
```

### 参考

#### 測定の参考にしたサイト
* [Python データベースコネクションをプーリング(SQLAlchemy使用) - Symfoware](https://symfoware.blog.fc2.com/blog-entry-1816.html)

#### SQLAlchemy関連

* [Connection Pooling — SQLAlchemy 1.4 Documentation](https://docs.sqlalchemy.org/en/14/core/pooling.html)