Использование партиций PostgreSQL c Django

Партицирование в Postgresql(и в MySQL :)) очень полезно при хранении больших масcивов данных, особенно если нам нужно с ними работать разделяя по конкретному критерию. В моем случае у меня есть логи работы над проектами и эти логи хранятся в партициях, по одной партиции на каждый проект.

Использование партицирования в Django не сложно, но есть один подводный камень. Django выполняет запрос на вставку с конструкцией “returning id” которая должна вернуть id вставленной записи. Но так как мы используем партицирование — у нас есть функция котороя делает вставку данных и возвращает тригер. Для того что бы это обойти мы просто будем просить django не использовать “returning id” и все.

Сейчас разберем как использовать партицирование шаг за шагом.

шаг 1: создаем модели

class Project(models.Model):
    title = models.CharField(max_length=150)

class ProjectLog(models.Model):
    project = models.ForeignKey(Project)
    user = models.ForeignKey(User)
    content = models.CharField(max_length=500)

шаг 2: создаем миграции, что бы добавить в базу данных логику партицирования у нас будет две функции: первая для создания новой таблици для каждого проекта и вторая для вставки данных в нужную таблицу

def make_partitions_operants(apps, schema_editor):
    Project = apps.get_model('project', 'Project')
    ProjectLog = apps.get_model('project', 'ProjectLog')

    sql = '''
        create or replace function insert_into_projectlog() returns trigger as $$
        begin
          execute 'insert into %(projectlog_db_table)s_' || NEW.project_id || ' values(($1).*)' using NEW;

          return NULL;
        end $$ language 'plpgsql';

        create or replace function create_projectlog_partition() returns trigger as $$
        begin
          execute 'create table %(projectlog_db_table)s_' || NEW.id || ' (
            check (project_id = ' || NEW.id || ')
          ) inherits (%(projectlog_db_table)s);';

          return NEW;
        end $$ language 'plpgsql';

        create trigger trigger_create_projectlog_partition
          after insert on %(project_db_table)s
          for each row execute procedure create_projectlog_partition();

        create trigger trigger_insert_into_projectlog
          before insert on %(projectlog_db_table)s
          for each row execute procedure insert_into_projectlog();
    ''' % {
        'projectlog_db_table': ProjectLog._meta.db_table,
        'project_db_table': Project._meta.db_table,
        'rowtype': '%%ROWTYPE',
    }

    schema_editor.execute(sql)


class Migration(migrations.Migration):

    dependencies = [
        ('project', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(make_partitions_operants),
    ]

я использую родные миграции для Django 1.7

шаг 3: нам нужно переопределить метод save у ProjectLog

def save(self, *args, **kwargs):
        from django.db.backends.postgresql_psycopg2.base import DatabaseFeatures

        if not self.pk:
            DatabaseFeatures.can_return_id_from_insert = False

        super(ProjectLog, self).save(*args, **kwargs)

        DatabaseFeatures.can_return_id_from_insert = True

здесь мы переписываем значение can_return_id_from_insert, чтобы в запросе не использовался “returning id”

Вот и все что нам потребовалось сделать.

Теперь каждый раз когда будет создаваться запись о новом проекте, в базе дыннх будет появляться новая таблица и все логи будут писаться по разным таблицам.

Пример как это работает:

test=# \dt
              List of relations
 Schema |        Name        | Type  | Owner
--------+--------------------+-------+-------
 public | project_project    | table | quard
 public | project_projectlog | table | quard
(2 rows)

test=# INSERT INTO project_project(title) VALUES('blog');
INSERT 0 1
test=# \dt
                   List of relations
 Schema |         Name         | Type  |     Owner
--------+----------------------+-------+----------------
 public | project_project      | table | quard
 public | project_projectlog   | table | quard
 public | project_projectlog_1 | table | vadymzakovinko
(3 rows)

test=# INSERT INTO project_project(title) VALUES('education');
INSERT 0 1
test=# \dt
                   List of relations
 Schema |         Name         | Type  |     Owner
--------+----------------------+-------+----------------
 public | project_project      | table | quard
 public | project_projectlog   | table | quard
 public | project_projectlog_1 | table | vadymzakovinko
 public | project_projectlog_2 | table | vadymzakovinko
(4 rows)

test=# INSERT INTO project_projectlog(user_id, project_id, content) VALUES(1, 1, 'new article');
INSERT 0 0
test=# SELECT * FROM project_projectlog;
 id | user_id | project_id |   content
----+---------+------------+-------------
  1 |       1 |          1 | new article
(1 row)

test=# SELECT * FROM project_projectlog_1;
 id | user_id | project_id |   content
----+---------+------------+-------------
  1 |       1 |          1 | new article
(1 row)

test=# SELECT * FROM project_projectlog_2;
 id | user_id | project_id | content
----+---------+------------+---------
(0 rows)

test=# INSERT INTO project_projectlog(user_id, project_id, content) VALUES(1, 2, 'django 1.7 migrations');
INSERT 0 0
test=# SELECT * FROM project_projectlog_2;
 id | user_id | project_id |        content
----+---------+------------+-----------------------
  2 |       1 |          2 | django 1.7 migrations
(1 row)

test=# SELECT * FROM project_projectlog_1;
 id | user_id | project_id |   content
----+---------+------------+-------------
  1 |       1 |          1 | new article
(1 row)

test=# SELECT * FROM project_projectlog;
 id | user_id | project_id |        content
----+---------+------------+-----------------------
  1 |       1 |          1 | new article
  2 |       1 |          2 | django 1.7 migrations
(2 rows)