具体化されたビューによる高速読み込み

28112 ワード

このブログは、PostgreSQLのビューのGISTとDJangoでの統合方法を提供します.
PostgreSQLのビューは非常に便利で、ビューを複雑なクエリをラッピング(抽象化)し、名前を割り当てることを考えます.
それらは、ビューを使用するいくつかの便利です
  • ビューは複雑さを隠す
  • 複数のテーブルを集約したり結合したりする複雑なロジックを必要とするクエリがある場合は、そのロジックをすべてビューに変換できます
    そして、それが通常のテーブルであるかのように、それからデータをretreiveします.
  • ビューはセキュリティに使用できます
  • あなたが特定の列/行の機密データを持つテーブルを持っていると想像してください、そして、あなたはそのデータを公開することなくユーザーにアクセスをしたいです、見解は我々に必要なデータだけをラップさせて、下にあるテーブルの代わりにその見方に許可を与えさせます.
    すべてのSQLデータベースPostgresと同様に2種類のビューがあります
  • 見解
  • 実体化された見解
  • 見解と具体化された見解のわずかな違いは、ここにあります
    見解
    実体化された見解
    ビューはディスクに保存されません.
    材料はディスクに保存されます.
    ビューからデータを読み込むたびに、基になるテーブルからデータが読み込まれます.
    具体化されたビューは独自のテーブルを持っているので、具体化されたビューからの読みは直接テーブルから読みますが、基になるテーブルから読みます.
    具体化された見解を実行しようとしましょう.ブログサイトの次のシナリオを考えてください.典型的なブログサイトモデルが含まれます
  • ブログ表
  • カテゴリー表
  • タグテーブル
  • それらの間の関係が非常に簡単であるところでは、ブログはカテゴリーに外部キーを持っていて、タグとの多くの多くの関係を持ちます.等価なDjangoモデルはこのようになります.
    
    class Tags(models.Model):
        name = models.CharField(max_length=200, unique=True)
    
        class Meta:
            verbose_name = "Tag"
            verbose_name_plural = "Tags"
            db_table = "tags"
    
        def __str__(self):
            return self.name
    
    
    class Category(models.Model):
        name = models.CharField(max_length=200, unique=True)
    
        class Meta:
            verbose_name = "Category"
            verbose_name_plural = "Categories"
            db_table = "categories"
    
        def __str__(self):
            return self.name
    
    
    class Blog(models.Model):
        title = models.CharField(max_length=200, unique=True)
        content = models.TextField()
        created_at = models.DateTimeField(auto_now=True)
        is_published = models.BooleanField(default=True)
        category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
        tags = models.ManyToManyField(Tags)
    
        class Meta:
            verbose_name = "Blog"
            verbose_name_plural = "Blogs"
            db_table = "blogs"
    
        def __str__(self):
            return self.title
    
    
    
    すべての3つのテーブルをクエリし、すべての3つのテーブルからデータを取得する必要がある状況を仮定します.これを実現するために、すべてのテーブルに参加する必要があります
    
    
    queryset = Blog.objects.select_related("category").prefetch_related("tags").all()
    
    # the resulting SQL query generated by Django ORM is
    
    SELECT "blogs"."id",
        "blogs"."title",
        "blogs"."content",
        "blogs"."created_at",
        "blogs"."is_published",
        "blogs"."category_id",
        "categories"."id",
        "categories"."name"
    FROM "blogs"
    LEFT OUTER JOIN "categories"
    ON ("blogs"."category_id" = "categories"."id")
    LIMIT 21
    
    SELECT ("blogs_tags"."blog_id") AS "_prefetch_related_val_blog_id",
           "tags"."id",
           "tags"."name"
      FROM "tags"
     INNER JOIN "blogs_tags"
        ON ("tags"."id" = "blogs_tags"."tags_id")
     WHERE "blogs_tags"."blog_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21)
    
    
    ここで2つのクエリを実際に実行していることに注意してください.
    
    SELECT b.id,
           b.title AS blog_title,
           b.created_at AS blog_created_at,
           b.is_published AS blog_is_published,
           b.category_id AS blog_category_id,
           b.content AS blog_content,
           c.id AS category_id,
           c.name AS category_name,
           bt.blog_id AS blog_tag_blog_id,
           count(t.id) AS tag_count,
           jsonb_agg(jsonb_build_object('tag_id', t.id, 'tag_name', t.name)) AS tag_json
    FROM blogs b
    LEFT JOIN categories c ON b.category_id = c.id
    LEFT JOIN blogs_tags bt ON b.id = bt.blog_id
    LEFT JOIN tags t ON bt.tags_id = t.id
    GROUP BY b.id,
             c.id,
             bt.blog_id
    ORDER BY b.id;
    
    
    
    クエリが非常に複雑であることがわかりますので、データにアクセスする必要があるたびにクエリを実行する必要があります.ありがたいことに、簡単な方法でそれを簡単にすることができます.
    我々が具体化された見解をどのようにつくることができるか見ましょう
    
    CREATE materialized VIEW mv_blogs_with_categories_and_tags_combined AS
    SELECT b.id,
           b.title AS blog_title,
           b.content AS blog_content,
           b.created_at AS blog_created_at,
           b.is_published AS blog_is_published,
           c.id AS category_id,
           c.name AS category_name,
           count(t.id) AS tag_count,
           jsonb_agg(jsonb_build_object('tag_id', t.id, 'tag_name', t.name)) AS tags
    FROM blogs b
    LEFT JOIN categories c ON b.category_id = c.id
    LEFT JOIN blogs_tags bt ON b.id = bt.blog_id
    LEFT JOIN tags t ON bt.tags_id = t.id
    GROUP BY b.id,
             c.id,
             bt.blog_id
    ORDER BY b.id WITH NO DATA;
    
    
    --- create a unique index, this is needed when we try to refresh the materialized view concurrently
    CREATE UNIQUE INDEX ON mv_blogs_with_categories_and_tags_combined (id);
    
    --- refresh the materialized view, to populate the data into it.
    REFRESH MATERIALIZED VIEW mv_blogs_with_categories_and_tags_combined;
    
    -------- refresh the materialized view concurrently, without locking the table
    -------- when we refresh the view without concurrently , a lock is acquired on the table
    -------- to prevent this we use concurrently
    REFRESH MATERIALIZED VIEW CONCURRENTLY mv_blogs_with_categories_and_tags_combined;
    
    
    
    以下のスクリーンショットでは、実行時間を見ることができます.複雑化されたクエリテーブルを実行するときに、表示されたビューテーブルVSに問い合わせます.

    Execution Time: 25.874 ms
    
    

    Execution Time: 0.155 ms
    
    

    スピードアップ


    すべてのコードはこちらhttps://github.com/sreevardhanreddi/django-materialized-views

    Djangoとの統合


    SQLが行われた今、私たちはDjangoとの統合を通して行きましょう
    このコマンドを使用して空の移行ファイルを作成します
    python manage.py makemigrations --name materialized_blogs pg_app --empty
    
    
    生成されるこのファイルは、Djangoのmigrateコマンドを使用して移動を作成するために使用されます
    
    # Generated by Django 3.2.6 on 2021-08-12 06:59
    
    from django.db import migrations
    from django.db.migrations.operations.special import RunSQL
    
    
    class Migration(migrations.Migration):
    
        dependencies = [
            ("pg_app", "0001_initial"),
        ]
    
        operations = [
            migrations.RunSQL(
                """
                CREATE materialized VIEW mv_blogs_with_categories_and_tags_combined AS
                SELECT b.id,
                    b.title AS blog_title,
                    b.content AS blog_content,
                    b.created_at AS blog_created_at,
                    b.is_published AS blog_is_published,
                    c.id AS category_id,
                    c.name AS category_name,
                    count(t.id) AS tag_count,
                    jsonb_agg(jsonb_build_object('tag_id', t.id, 'tag_name', t.name)) AS tags
                FROM blogs b
                LEFT JOIN categories c ON b.category_id = c.id
                LEFT JOIN blogs_tags bt ON b.id = bt.blog_id
                LEFT JOIN tags t ON bt.tags_id = t.id
                GROUP BY b.id,
                        c.id,
                        bt.blog_id
                ORDER BY b.id WITH NO DATA;
                """,
                """
                    DROP MATERIALIZED VIEW IF EXISTS mv_blogs_with_categories_and_tags_combined;
                """,
            ),
            migrations.RunSQL(
                """
                REFRESH MATERIALIZED VIEW mv_blogs_with_categories_and_tags_combined;
                """,
                """
                REFRESH MATERIALIZED VIEW mv_blogs_with_categories_and_tags_combined;
                """,
            ),
            migrations.RunSQL(
                """
                CREATE UNIQUE INDEX idx_mv_blogs_with_categories_and_tags_combined_id ON mv_blogs_with_categories_and_tags_combined (id);
                """,
                """
                DROP INDEX IF EXISTS idx_mv_blogs_with_categories_and_tags_combined_id
                """,
            ),
        ]
    
    
    # RunSQL allows you to write code for migrating forwards and backwards
    # i.e, applying migrations and unapplying them. here
    # the first string in RunSQL is the forward SQL, the second is the reverse SQL
    
    
    
    SQLコマンドを追加したらpython manage.py migrate これはデータベースへの移行を適用します.
    このモデルの後、具体化されたビューを反映するモデル
    
    class BlogsWithCategoriesAndTagsCombined(models.Model):
        id = models.IntegerField(primary_key=True)
        blog_title = models.CharField(max_length=200, unique=True)
        blog_content = models.TextField()
        blog_created_at = models.DateTimeField()
        blog_is_published = models.BooleanField()
        category_id = models.IntegerField()
        category_name = models.CharField(max_length=200)
        tag_count = models.IntegerField()
        tags = models.JSONField()
    
        class Meta:
            managed = False
            db_table = "mv_blogs_with_categories_and_tags_combined"
    
        def __str__(self):
            return self.blog_title
    
    
    ここでメタクラスに注意してくださいmanaged = False , これは、データベースでこのモデルのテーブルを作成しないようにDjangoに指示します.
    これが完了すると、それはQuery
    
    queryset = BlogsWithCategoriesAndTagsCombined.objects.all()
    
    
    具体化されたビューはデータのテーブルに依存しているので、テーブルが更新されると、手動でビュー化されたビューを手動で更新する必要があります.これはDjango crontabまたはセロリを使用してcronジョブ内で行うことができます.
    これはもともとはこちらhttps://sreevardhanreddi.github.io/