【譯】如何讓資料庫工效翻十倍

語言: CN / TW / HK

本篇譯文是二合一,原文作者是 Yury Selivanov(@1st1)和 Elvis Pranskevichus(@elprans),原文《A Path to a 10x Database》釋出於 2019 年 6 月 18 日,後半段摘自發佈於 2019 年 4 月 11 日的《EdgeDB 1.0 Alpha 1》,因時間較久,一些內容已不適用,因此我把有用的部分摘抄出來,譯作本文。原文沒有對 10x 做出解釋,10x 的一種說法來源於“優秀的工程師通常有 10 倍與常人的效率”,那麼一個 10x 資料庫就意味著,資料庫的執行效率與使用者的工作效率將能“翻十倍”。今天是農曆正月初一,祝大家虎年 10x!

開端

我們最早是從一家有理想的外包公司起身,與其他許多人一樣,在資源有限的現實中,平衡對質量的追求對我們來說也是一件很糾結的事情。

幸運的是,我們在這段時間裡得以服務許多不同行業的專案,於是自然地積累下了一些內部框架,用以更快交付的同時,可以有更讓人愉悅的過程和結果。而這些框架中的關鍵元素就是我們的資料庫抽象整合,當時可以做到比如:

  • 在 schema 層面定義資料許可權規則,因此不需要寫重複的查詢語句。
  • 通過 schema 自省和標註,大規模自動生成圖形介面、API 介面以及帶有完整校驗功能的資料輸入表單。
  • 抽象出一些可重用的 schema 定義和查詢,簡單拼裝就能實現很多不同的常見應用模式。

這些想法的成功,激發了我們進一步開發它們的動力,於是就有了現在的 EdgeDB。

使命

EdgeDB 的使命是賦予使用者更多操作資料的能力靈活度自信,無論是構建服務還是資料研究,亦或是 SRE 持續部署的工作。這聽上去有些拗口,那我們看一下 EdgeDB 有哪些功能:

  • 無需 ORM 或其他抽象層的繁重開銷,即可與任何程式語言有效地接駁;
  • 使用如 JSON 或 Apache Arrow 這樣的常見序列化格式高效收發資料;
  • 亦可通過如 REST 和 GraphQL 等常見應用層協議對接客戶端;
  • 支援精心設計便於審計的安全策略,集中定義資料與 schema 的安全性(2.0 規劃中,譯註);
  • 內建 schema migration 與歷史記錄。
  • 大型多功能標準庫,包含基礎型別、常用函式和操作符。

為了實現這些功能,我們設計出了一款更強大的現代化查詢語言以替換 SQL 語言,更新了關係資料庫中 schema 的設計方式,並編寫出了一整套在開發和生產環境中高效操作資料庫所必須的工具。

設計理念

適用性是指,在特定環境下,特定使用者完成特定目標時的有效性、工作效率和滿意程度。

——ISO 9241(人機互動中的工效學)

EdgeDB 的設計遵循以下理念:

  • 符合人類工效學譯註:也就是追求最佳工作效率

操作 EdgeDB 的方方面面——如資料模型、EdgeQL——都應易學易懂,使用者體驗滿分。

  • 執行效率高

EdgeQL 的各項功能、不同程式語言的客戶端以及各類工具的設計都應考慮到高效低延遲。

  • 保證正確性

絕不因工效或效率而犧牲正確性。不符合邏輯的行為必須報錯。

以上這些不僅適用於資料庫核心及其查詢語言的設計,也適用於使用者在程式語言、作業系統及開發工作流中與資料庫的互動。

實戰對比

口說無憑,下面我們一起嘗試用不同的框架,實現相同的一個小 App,以觀察 EdgeDB 在開發效率、執行效率和正確性方面,如何做到工效翻十倍的。譯者加。

假設我們想要用 Python 做一個豆瓣電影,功能包括:

  • 電影(Movie)頁面展示電影名、發行年份、簡介、海報圖、導演和演員列表、總評分以及影評列表,每條影評(Review)又包括影評內容和評分,以及提交影評的使用者的名字和頭像。
  • 人物(Person)頁面展示姓名、照片、生平,以及導演或參演過得電影列表,其中每部電影又包括海報圖、電影名、發行年份和總評分。
  • 使用者(User)頁面展示名字和頭像,以及最近 10 條影評,每條影評又包括影評內容和評分,以及所評價電影的名稱、海報圖和總評分。

豆瓣最早可能也就是這樣吧,資料結構如下圖所示:

用於選型比較的技術棧有:Django + PostgreSQL、SQLAlchemy + PostgreSQL、手寫 SQL + PostgreSQL、MongoDB 和 EdgeDB。

可以在這裡下載本文用到的全部程式碼。

Django

Django 作為一款使用廣泛的 Python 框架,自帶一個簡易 ORM,但足以滿足我們的 schema 要求。寫查詢也相對直接,基本上就是 list 和 dict 等常規 Python 程式碼,加上一點查詢構建:

def render(self, movie):
    result = {}

    if movie:
        directors = [rel.person for rel in
                        movie.directors_rel.order_by(
                        'list_order', 'person__last_name'
                        ).select_related('person')]
        cast = [rel.person for rel in
                movie.cast_rel.order_by(
                    'list_order', 'person__last_name'
                ).select_related('person')]
        reviews = movie.reviews \
                        .order_by('-creation_time').select_related('author')

        result = {
            'id': movie.id,
            'image': movie.image,
            'title': movie.title,
            'year': movie.year,
            'description': movie.description,
            'directors': [{
                'id': person.id,
                'full_name': person.get_full_name(),
                'image': person.image,
            } for person in directors],
            'cast': [{
                'id': person.id,
                'full_name': person.get_full_name(),
                'image': person.image,
            } for person in cast],
            'avg_rating': movie.get_avg_rating(),
            'reviews': [{
                'id': review.id,
                'body': review.body,
                'rating': review.rating,
                'author': {
                    'id': review.author.id,
                    'name': review.author.name,
                    'image': review.author.image,
                },
            } for review in reviews],
        }

    return result

值得注意的是,雖然類似 Django 這樣的 REST 框架可以極大地簡化資料抓取和響應生成,但這通常會帶來巨大的效能開銷。

SQLAlchemy

SQLAlchemy 是一款強大的 Python ORM,支援各種型別的關係型 schema 設計,允許物件關係的微調,或者指定不同的關係載入方法,以生成比預設 Django 更高效的查詢語句。

這個特性也有額外開銷——想要充分發揮 SQLAlchemy 的所有潛能,使用者需要面對一堆相當複雜的 API,如各種載入策略和預製查詢等。SQLAlchemy 允許使用者做底層優化,那使用者就得同時精通 SQLAlchemy SQL 本身,才能達到最佳執行效率。

def get_movie(sess, id):
    # ...
    NULLS_LAST = 2 ^ 64

    def sort_key(rel):
        if rel.list_order is None:
            return (NULLS_LAST, rel.person_rel.last_name)
        else:
            return (rel.list_order, rel.person_rel.last_name)

    baked_query = bakery(lambda sess: (
        sess.query(m.Movie)
            .options(
                orm.subqueryload(m.Movie.directors_rel)
                .joinedload(m.Directors.person_rel, innerjoin=True),

                orm.subqueryload(m.Movie.cast_rel)
                .joinedload(m.Cast.person_rel, innerjoin=True),

                orm.subqueryload(m.Movie.reviews)
                .joinedload(m.Review.author, innerjoin=True),
            )
        )
    )

    baked_query += lambda q: q.filter_by(id=sa.bindparam('id'))

    movie = baked_query(sess).params(id=id).first()

    directors = [rel.person_rel for rel in
                 sorted(movie.directors_rel, key=sort_key)]

    cast = [rel.person_rel for rel in
            sorted(movie.cast_rel, key=sort_key)]

    result = {
        'id': movie.id,
        'image': movie.image,
        'title': movie.title,
        'year': movie.year,
        'description': movie.description,
        'avg_rating': float(movie.avg_rating),
        'directors': [
            {
                'id': d.id,
                'full_name': d.full_name,
                'image': d.image,
            } for d in directors
        ],
        'cast': [
            {
                'id': c.id,
                'full_name': c.full_name,
                'image': c.image,
            } for c in cast
        ],
        'reviews': [
            {
                'id': r.id,
                'body': r.body,
                'rating': float(r.rating),
                'author': {
                    'id': r.author.id,
                    'name': r.author.name,
                    'image': r.author.image,
                }
            } for r in sorted(movie.reviews,
                              key=lambda x: x.creation_time,
                              reverse=True)
        ]
    }

    return json.dumps(result)

MongoDB

MongoDB 是一個文件資料庫,使用者現在可以通過“聚合流水線”來執行一些不算簡單的查詢了,但對我們的 App 來說,相對於 Django、SQLAlchemy 甚至純 SQL,用 MongoDB 來寫查詢還是很有挑戰的。

MongoDB 的查詢本質上就是一些底層資料的表示式,類似 SQL 資料庫中查詢計劃輸出的結果,因此很難說我們的查詢到底是不是最優的。通常來看,MongoDB 的查詢會較難調整或重構,而使用多個子查詢語句來組合出一條查詢語句是基本上不可能的。

def get_movie(db, id):
    movie = db.movies.aggregate([
        {
            '$match': {
                '_id': id
            }
        },
        {
            '$lookup': {
                'from': 'people',
                'localField': 'cast',
                'foreignField': '_id',
                'as': 'cast'
            }
        },
        {
            '$lookup': {
                'from': 'people',
                'localField': 'directors',
                'foreignField': '_id',
                'as': 'directors'
            }
        },
        {
            '$lookup': {
                'from': 'reviews',
                'foreignField': 'movie',
                'localField': '_id',
                'as': 'reviews'
            }
        },
        {
            '$unwind': {
                'path': "$reviews",
                'preserveNullAndEmptyArrays': True
            }
        },
        {
            '$lookup': {
                'from': 'users',
                'localField': 'reviews.author',
                'foreignField': '_id',
                'as': 'reviews.author'
            }
        },
        {
            '$sort': {"reviews.creation_time": -1},
        },
        {
            '$group': {
                '_id': "$_id",
                'title': {'$first': "$title"},
                'year': {'$first': "$year"},
                'image': {'$first': "$image"},
                'description': {'$first': "$description"},
                'cast': {'$first': "$cast"},
                'directors': {'$first': "$directors"},
                'reviews': {'$push': "$reviews"}
            }
        },
        {
            '$project': {
                'cast': {  # 計算 `full_name` 會造成大約 5% 的額外開銷,
                           # 但其他所有測試都這麼做了,所以 mongodb 不冤,
                           # 正好看看它能否處理好這類查詢。
                    '$map': {
                        'input': '$cast',
                        'as': 'c',
                        'in': {
                            'name': {
                                "$concat": [
                                    "$$c.first_name",
                                    " ",
                                    {
                                        '$cond': {
                                            'if': {
                                                '$eq': ['$$c.middle_name', '']
                                            },
                                            'then': '',
                                            'else': {
                                                "$concat": [
                                                    "$$c.middle_name", ' '
                                                ]
                                            }
                                        }
                                    },
                                    "$$c.last_name"
                                ]
                            },
                            'image': '$$c.image',
                            '_id': '$$c._id',
                        }
                    }
                },
                'directors': {  # 見“cast”的註釋
                    '$map': {
                        'input': '$directors',
                        'as': 'c',
                        'in': {
                            'name': {
                                "$concat": [
                                    "$$c.first_name",
                                    " ",
                                    {
                                        '$cond': {
                                            'if': {
                                                '$eq': ['$$c.middle_name', '']
                                            },
                                            'then': '',
                                            'else': {
                                                "$concat": [
                                                    "$$c.middle_name", ' '
                                                ]
                                            }
                                        }
                                    },
                                    "$$c.last_name"
                                ]
                            },
                            'image': '$$c.image',
                            '_id': '$$c._id',
                        }
                    }
                },
                'reviews': 1,
                'image': 1,
                'title': 1,
                'year': 1,
                'description': 1,
                'avg_rating': {'$avg': '$reviews.rating'}
            }
        }
    ])
    movie = list(movie)
    result = bson.json_util.dumps(movie[0])
    return result

手寫 SQL

使用關係資料庫時,手寫 SQL 不失為一種十分高效的方式,因為你可以完全掌控資料的獲取方式。但在這種情況下,資料庫與客戶端的選擇變的尤其重要。比如說 PostgreSQL 從 11.0 起,支援聚合任意表達式的陣列,進一步允許我們從一個深層巢狀的資料結構中,用一條查詢語句就可以製造出最理想的返回結果(比如免除 JOIN 所帶來的冗餘資料)。然而遺憾的是,很多 PostgreSQL 的客戶端(包括最流行的 Python 客戶端——psycopg2)都沒有辦法直接解析這樣的查詢結果。

因此,我們將測試兩種客戶端:psycopg2 以及我們自己的 asyncpg。

psycopg2

對於 psycopg2,我們不得不把查詢拆分成多條語句,這很像 Django 和 SQLAlchemy 在底層的實現。同樣是用來渲染電影頁面的程式碼:

def get_movie(conn, id):
    with conn.cursor() as cur:
        cur.execute('''
            SELECT
                movie.id,
                movie.image,
                movie.title,
                movie.year,
                movie.description,
                movie.avg_rating
            FROM
                movies AS movie
            WHERE
                movie.id = %s;
        ''', [id])

        movie_rows = cur.fetchall()
        movie = movie_rows[0]

        cur.execute('''
            SELECT
                person.id,
                person.full_name,
                person.image
            FROM
                directors
                INNER JOIN persons AS person
                    ON (directors.person_id = person.id)
            WHERE
                directors.movie_id = %s
            ORDER BY
                directors.list_order NULLS LAST,
                person.last_name
        ''', [id])
        directors_rows = cur.fetchall()

        cur.execute('''
            SELECT
                person.id,
                person.full_name,
                person.image
            FROM
                actors
                INNER JOIN persons AS person
                    ON (actors.person_id = person.id)
            WHERE
                actors.movie_id = %s
            ORDER BY
                actors.list_order NULLS LAST,
                person.last_name
        ''', [id])
        cast_rows = cur.fetchall()

        cur.execute('''
            SELECT
                review.id,
                review.body,
                review.rating,
                author.id AS author_id,
                author.name AS author_name,
                author.image AS author_image
            FROM
                reviews AS review
                INNER JOIN users AS author
                    ON (review.author_id = author.id)
            WHERE
                review.movie_id = %s
            ORDER BY
                review.creation_time DESC
        ''', [id])
        reviews_rows = cur.fetchall()

    return json.dumps({
        'id': movie[0],
        'image': movie[1],
        'title': movie[2],
        'year': movie[3],
        'description': movie[4],
        'avg_rating': str(movie[5]),

        'directors': [
            {
                'id': d[0],
                'full_name': d[1],
                'image': d[2]
            } for d in directors_rows
        ],

        'cast': [
            {
                'id': c[0],
                'full_name': c[1],
                'image': c[2]
            } for c in cast_rows
        ],

        'reviews': [
            {
                'id': r[0],
                'body': r[1],
                'rating': r[2],
                'author': {
                    'id': r[3],
                    'name': r[4],
                    'image': r[5]
                }
            } for r in reviews_rows
        ]
    })

用這種方法來獲取資料並不複雜,並且效能也非常符合預期。但必須說明的是,上面的程式碼是有瑕疵的,因為在併發的情況下有可能發生資料爭用。要避免這種情況,就需要在允許 REPEATABLE READ 的事務中來執行所有查詢。這種情況同樣適用於各類 ORM,比如在 Django 中,如果不作任何處理的話,Django 會預設使用“自動提交”模式,所以每個查詢都有可能是用了不同的資料庫快照。

asyncpg

對於 asyncpg,客戶端和伺服器都會使用二進位制資料編碼,因此可以處理任何型別的巢狀資料,所以我們可以用最理想的方式來獲取資料:

async def get_movie(conn, id):
    # 以下查詢語句需要 PostgreSQL 11,並且只有 asyncpg 能處理其結果

    movie = await conn.fetch('''
        SELECT
            movie.id,
            movie.image,
            movie.title,
            movie.year,
            movie.description,
            movie.avg_rating,

            (SELECT
                COALESCE(array_agg(q.v), (ARRAY[])::record[])
             FROM
                (SELECT
                    ROW(
                        person.id,
                        person.full_name,
                        person.image
                    ) AS v
                FROM
                    directors
                    INNER JOIN persons AS person
                        ON (directors.person_id = person.id)
                WHERE
                    directors.movie_id = movie.id
                ORDER BY
                    directors.list_order NULLS LAST,
                    person.last_name
                ) AS q
            ) AS directors,

            (SELECT
                COALESCE(array_agg(q.v), (ARRAY[])::record[])
             FROM
                (SELECT
                    ROW(
                        person.id,
                        person.full_name,
                        person.image
                    ) AS v
                FROM
                    actors
                    INNER JOIN persons AS person
                        ON (actors.person_id = person.id)
                WHERE
                    actors.movie_id = movie.id
                ORDER BY
                    actors.list_order NULLS LAST,
                    person.last_name
                ) AS q
            ) AS actors,


            (SELECT
                COALESCE(array_agg(q.v), (ARRAY[])::record[])
             FROM
                (SELECT
                    ROW(
                        review.id,
                        review.body,
                        review.rating,
                        (SELECT
                            ROW(
                                author.id,
                                author.name,
                                author.image
                            )
                            FROM
                                users AS author
                            WHERE
                                review.author_id = author.id
                        )
                    ) AS v
                FROM
                    reviews AS review
                WHERE
                    review.movie_id = movie.id
                ORDER BY
                    review.creation_time DESC
                ) AS q
            ) AS reviews
        FROM
            movies AS movie
        WHERE
            id = $1;
    ''', id)

    movie = movie[0]

    return json.dumps({
        'id': movie['id'],
        'image': movie['image'],
        'title': movie['title'],
        'year': movie['year'],
        'description': movie['description'],
        'avg_rating': float(movie['avg_rating']),

        'directors': [
            {
                'id': d[0],
                'full_name': d[1],
                'image': d[2],
            } for d in movie['directors']
        ],

        'cast': [
            {
                'id': c[0],
                'full_name': c[1],
                'image': c[2],
            } for c in movie['actors']
        ],

        'reviews': [
            {
                'id': r[0],
                'body': r[1],
                'rating': r[2],
                'author': {
                    'id': r[3][0],
                    'name': r[3][1],
                    'image': r[3][2],
                }
            } for r in movie['reviews']
        ]
    })

實際上這還挺不錯的!我們用一條查詢語句就獲得了所有資料,根本不需要擔心資料爭用和資料庫事務。但關鍵在於,老版本的 PostgreSQL 並不支援這樣的查詢,並且也不是所有的資料庫客戶端都能處理這樣的查詢結果。

EdgeDB

資料結構

使用 EdgeDB 的第一步就是定義資料結構。下面的程式碼定義了兩個物件型別:Review 和 Movie(完整 schema 定義在這裡)。此處使用的是 schema 定義語言(SDL),我們在語言的可讀性和表達力上花費了大量的精力:

type Review {
    required property body -> str;
    required property rating -> int64 {
        constraint min_value(0);
        constraint max_value(5);
    }

    required link author -> User;
    required link movie -> Movie;

    required property creation_time -> cal::local_datetime {
        default := cal::to_local_datetime(datetime_current(), 'UTC')
    }
}

type Movie {
    required property title -> str;
    required property year -> int64;
    required property description -> str;

    multi link directors -> Person;
    multi link cast -> Person;

    property avg_rating := math::mean(.<movie[IS Review].rating);
}

EdgeQL

EdgeQL 是 EdgeDB 最重要的查詢語言,用一條語句就可以完成各種巢狀結構的查詢,返回 JSON 字串形式的結果,或是不同程式語言所適用的資料結構。

比如返回 JSON 字串的查詢:

def get_movie(conn, id):
    return conn.query_single_json('''
        SELECT Movie {
            id,
            image,
            title,
            year,
            description,
            avg_rating,

            directors: {
                id,
                full_name,
                image,
            }
            ORDER BY @list_order EMPTY LAST
                THEN .last_name,

            cast: {
                id,
                full_name,
                image,
            }
            ORDER BY @list_order EMPTY LAST
                THEN .last_name,

            reviews := (
                SELECT Movie.<movie[IS Review] {
                    id,
                    body,
                    rating,
                    author: {
                        id,
                        name,
                        image,
                    }
                }
                ORDER BY .creation_time DESC
            ),
        }
        FILTER .id = <uuid>$id
    ''', id=id)

或者返回 Python 物件結構,以便於在手動 JSON 序列化之前,從服務端對資料進行進一步的處理:

def get_movie(conn, id):
    m = conn.query_single('''
        ... 跟上面一模一樣的 EdgeQL 查詢語句 ...
    ''', id=id)

    return json.dumps({
        'id': str(m.id),
        'image': m.image,
        'title': m.title,
        'year': m.year,
        'description': m.description,
        'avg_rating': m.avg_rating,

        'directors': [
            {
                'id': str(d.id),
                'full_name': d.full_name,
                'image': d.image,
            } for d in m.directors
        ],

        'cast': [
            {
                'id': str(c.id),
                'full_name': c.full_name,
                'image': c.image,
            } for c in m.cast
        ],

        'reviews': [
            {
                'id': str(r.id),
                'body': r.body,
                'rating': r.rating,
                'author': {
                    'id': str(r.author.id),
                    'name': r.author.name,
                    'image': r.author.image,
                }
            } for r in m.reviews
        ]
    })

GraphQL

最後,EdgeDB 還支援開箱即用的 GraphQL。雖然不如 EdgeQL 一樣強大,但 GraphQL 可以直接使用在 SDL 中定義的複雜物件檢視,間接支援計算屬性和反向連結查詢。

query movie($id: ID!) {
    movie: GraphQLMovieDetails(filter: {id: {eq: $id}}) {
        id
        image
        title
        year
        description
        directors {
            id
            full_name
            image
        }
        cast {
            id
            full_name
            image
        }
        avg_rating
        reviews(order: {creation_time: {dir: DESC}}) {
            id
            body
            rating
            author {
                id
                name
                image
            }
        }
    }
}

效能評測

有了這些測試 App 的不同實現,我們就可以對比評測執行效能了。

我們使用 Python 的 NLTK 庫生成了一份包含 10 萬人物、10 萬用戶、約 60 萬影評和超過 2.5 萬電影的測試資料集,其中又有錯綜複雜的關聯關係。姓名根據美國人口普查資料隨機生成,而標題、文字、簡介和影評的生成則是儘量模擬了現實生活中的真實語言,也照顧到了真實影評類應用常見的文字長度和大概的寫作特點。資料集的大小特意設計為記憶體裝得下,但又有足夠的分量去測試出查詢的質量。

我們使用了下面的配置來執行測試:

  • 資料庫跑在一臺 12 核的 GCP 虛擬機器上,有 16G 記憶體和一塊 SSD 硬碟。
  • 測試指令碼跑在另外一臺 8 核的 GCP 虛擬機器上,有 12G 記憶體和一塊 SSD 硬碟。
  • 併發量設定為 24,這對應了 Python 測試指令碼的 24 個獨立作業系統程序,每個程序有一個獨立的資料庫連線;用 Go 語言寫的測試指令碼則有 24 個 goroutine 客戶端。
  • 每次測試都有 10 秒鐘的熱機時間,接著是 30 秒不間斷的測試。

另請見完整的報告評測程式碼庫

分析

首先需要著重強調的是,儘管這些查詢看起來普普通通,但實際上資料庫需要高效完成許多工,才能滿足這些查詢的效率。每種測試中,SQL 查詢都要 JOIN 多張表,用聚合函式算出電影的總評分;而對於 MongoDB 的各個查詢,我們使用了聚合框架來組合不同的查詢結果。

測試結果的一些解讀:

  • 我們觀察到,Django ORM 的瓶頸在於 CPU——低效率的 Python 程式碼太多了。不同於 SQLAlchemy,Django 缺少查詢生成的微調機制,你甚至不能控制一個檢視有多少次資料庫查詢。

    加上 Django REST 框架之後變得更慢了(見完整報告)。

  • 為了提高 SQLAlchemy 的效能,我們做了大量的調整和實驗,其中最為重要的就是“預製”查詢,和選一個最優的載入策略。如果不使用這些高階特性,SQLAlchemy 的效能也就跟 Django ORM 差不多。

  • MongoDB 效能差還挺讓人吃驚的。一開始我們盲猜雖然聚合框架可能比較慢,但是按 ID 來索取文件至少應該很快。但打臉的是,對於等價的按 ID 索取操作,PostgreSQL 仍然比 MongoDB 快不少。另外,MongoDB 除了資料庫 JOIN 比較慢之外,它的 Python 客戶端也不怎麼快——BSON 到 JSON 的轉換消耗了大量 CPU 時間。

  • PostgreSQL 與 psycopg2 的配合表現相當不錯,而它輸給 asyncpg 的兩個限制性因素為:一次呼叫需要執行多次查詢,以及使用更慢的基於文字的資料編碼方式。通過 psycopg2 我們可以確定,在 Python 中使用 ORM 能讓請求變慢 5 至 10 倍。

  • EdgeDB 表現相當出色。相較於所有其他解決方案,EdgeDB 能用少得多的程式碼來完成建表和查詢。

    通過 HTTP 來使用 GraphQL 和 EdgeQL 的效能很好,即使 HTTP 帶來了額外的開銷。這意味著,EdgeDB 已經可以用於任何支援 HTTP 的程式語言了。

    “EdgeDB repack”測試使用了阻塞式 IO 的 Python 客戶端,查詢返回 Python 物件形式的資料,再手動編碼成 JSON;而“EdgeDB JSON asyncio”測試則是使用了非阻塞式的 Python asyncio 客戶端,並讓 EdgeDB 直接返回 JSON 字串。

    EdgeDB 的 Python 客戶端經過了大量的優化,因此幾乎不怎麼佔用 CPU 時間。阻塞式測試與 asyncio 測試的效能不分伯仲,但後者可以更高效地利用 CPU 資源,使用的記憶體卻少很多。

    最後,我們知道現在的 EdgeDB 還有一些不足之處,會在將來的版本中修復,到時候效能應該會有進一步的提升。

  • PostgreSQL 與 asyncpg 是最佳效能組合,給未來版本的 EdgeDB 提供了追趕的目標。但是要知道,asyncpg 是世界上最快的 PostgreSQL 驅動之一,並且日常工作中我們一般不會寫出如測試中那般的查詢語句。

結論

通過我們的測評可以看出,即使相較於使用世界上頂尖的 RDBMS、榜首的驅動且人工優化過的 SQL,EdgeDB 先進的查詢語言仍然可以為開發者提供十分具有競爭力的執行效能。

受限於簡單的資料結構和查詢語句,這篇文章中的示例並不能展現出 EdgeQL 的全部潛能,比如 EdgeQL 可以不費吹灰之力地增加巢狀深度,而這對於其他選項來說難度會顯著增加。(與此同時,EdgeDB 還保證了正確性,並大幅降低了開發者的心智磨損,配合出色的效能表現,工作效率何止“翻十倍”。譯者加。)

歡迎關注我們的官方網站掘金專欄知乎專欄OSCHINA 專案主頁,瞭解更多資訊。