likes
comments
collection
share

数据库性能优化(二): PostgreSQL分区

作者站长头像
站长
· 阅读数 5

前言

上文提到了采用orm(ent.io)实现水平分表,来优化数据的读写性能。此方案也存在弊端,1. 跨分表进行查询、分页、排序的复杂度变高了。2.业务层代码是需要对应调整。那如何解决这些问题呢,可以尝试数据库(PostgreSQL)分区,和分表一样,分区亦旨在提高查询性能、简化数据管理。今天将简介分区的概念、优势和通过重构用户表的例子来演示ent.io实现分区的过程。

PostgreSQL分区

  1. 什么是分区?

    • 分区是指将逻辑上的一个大表分割成更小的物理部分。通过将数据分散到不同的分区中,可以提高查询性能和维护效率。
  2. 优势

    • 提高查询性能:分区可以单个减少数据集的大小,且分区有效地替代了索引的上层树,使得索引的频繁使用的部分更有可能适合内存。当表中大多数访问频繁的行位于单个分区或少量分区中时,显著提高查询效率。
    • 简化维护:具有分区的表可以更容易地进行备份、恢复和维护,而无需操作整个表。
    • 代码解耦:分区的实现逻辑由数据库实现、非业务层面。业务层更加简洁、且在物理层面和索引的加持,性能更优
  3. 策略

    • Range 范围分区: 该表被分区为由键列或列集定义的“范围” ,分配给不同分区的值范围之间没有重叠。例如,可以按日期范围或特定业务对象的标识符范围进行分区。每个范围的界限被理解为包括下端和不包括上端。例如,如果一个分区的范围是从110,下一个分区的范围是从1020,则值10属于第二个分区而不是第一个分区。
    • List 列表分区: 通过显式列出每个分区中出现的键值来对表进行分区。
    • Hash 哈希分区: 通过为每个分区指定模数和余数来对表进行分区。每个分区将保存分区键的哈希值除以指定模数将产生指定余数的行。

quick start

接着我们以官网的例子来实践一下分区的步骤。大致如下

  1. 创建分区表:定义表结构和分区规则。
  2. 创建分区:根据分区规则创建具体的分区。
  3. 管理分区数据:插入、更新和删除数据。
  4. 查询验证: 通过explain分析、验证是分区查询。

开启enable_partition_pruning

Postgre分区需要修改postgresql.conf,去掉注释enable_partition_pruning = on 检查是否开启:

SHOW enable_partition_pruning;
on

1. 创建分区表

通过指定子句将表创建measurement为分区表PARTITION BY该子句包括分区方法(RANGE在本例中)和用作分区键的列列表。

CREATE TABLE measurement (
    city_id int not null,
    logdate date not null,
    peaktemp int,
    unitsales int
) PARTITION BY RANGE (logdate);

2. 创建分区

每个分区的定义必须指定与父分区的分区方法和分区键相对应的边界。

CREATE TABLE measurement_y2006m02 PARTITION OF measurement
FOR VALUES FROM ('2006-02-01') TO ('2006-03-01');
CREATE TABLE measurement_y2006m03 PARTITION OF measurement
FOR VALUES FROM ('2006-03-01') TO ('2006-04-01');
CREATE TABLE measurement_y2007m01 PARTITION OF measurement
FOR VALUES FROM ('2007-01-01') TO ('2007-02-01');
CREATE TABLE measurement_y2007m11 PARTITION OF measurement
FOR VALUES FROM ('2007-11-01') TO ('2007-12-01');

查询是否创建成功

SELECT
    tablename
    FROM
    pg_tables
    WHERE
    schemaname = 'public' -- 替换为您的模式名称,如不同
AND tablename LIKE 'measurement_%'; -- 替换为您的分区表的名称模式

OUTPUT

    measurement_y2006m02
    measurement_y2006m03
    measurement_y2007m01
    measurement_y2007m11

输出如上,表示成功。

3. 管理分区数据

插入、更新和删除数据。往分区表插入数据

INSERT INTO "public"."measurement" ("city_id", "logdate", "peaktemp", "unitsales") VALUES (1, '2006-02-01', 2, 3);

4. 查询验证

如我们要查询2007-01-01之后的数据

explain analyze
SELECT *
FROM measurement
WHERE logdate > '2007-01-01';

//OUTPUT
//Seq Scan on measurement_y2007m11 measurement (cost=0.00..33.12 rows=617 width=16) (actual time=0.004..0.004 rows=0 loops=1)
// Filter: (logdate > '2007-01-01'::date)
// Planning Time: 0.091 ms
// Execution Time: 0.026 ms

可见,查询根据Range规则,从分区measurement_y2007m11从查询。达到分区的目的。

Ent.io实现分区

我们对上一篇的User Example使用分区进行重构。大致的步骤和上例一致。详细如下:

package main

import (
	"context"
	"database/sql"
	"fmt"
	_ "github.com/lib/pq"
	"log"
	"strings"
	"time"
	"tkingo.vip/egs/ent-partition/ent"
	"tkingo.vip/egs/ent-partition/ent/user"
)

func main() {
	var (
		ctx       = context.Background()
		startTime = time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)
		endTime   = time.Date(2024, time.March, 30, 23, 59, 59, 0, time.UTC)
	)
	dsn := "host=localhost user=postgres password=123456 dbname=test_partition port=6432 sslmode=disable TimeZone=Asia/Shanghai"
	client, err := ent.Open("postgres", dsn, ent.Debug())
	if err != nil {
		log.Fatalf("failed opening connection to postgres: %v", err)
	}
	defer client.Close()

	db, err := sql.Open("postgres", "postgres://postgres:123456@localhost:6432/test_partition?sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}
	db.SetConnMaxLifetime(time.Hour)
	db.SetMaxOpenConns(30)
	defer db.Close()

	// 1。创建分区表
	tableName := "users"
	createSql := `CREATE TABLE users (
		    id         serial NOT NULL,
		    age         int not null,
		    name        varchar,
		    date        timestamptz not null
		) PARTITION BY RANGE (date);`
	_, err = db.Exec(createSql)
	if err != nil {
		log.Fatalf("failed creating schema resources: %v", err)
	}

	// 2。创建分区
	now := time.Now()
	yearTables := getYearlyTables(now.Year())
	monthPo := time.January
	monthsFirst := GetYearlyMonthDate(2024) //计算每月的起止

	for _, monthTable := range yearTables {
		startEnd := monthsFirst[monthPo]
		query := fmt.Sprintf(`CREATE TABLE %s PARTITION OF %s
            FOR VALUES FROM ('%s') TO ('%s');`, monthTable, tableName, startEnd.Start, startEnd.End)
		_, err = db.Exec(query)
		if err != nil {
			log.Fatalf("failed creating schema resources: %v", err)
		}
		monthPo++
	}

	// 3。管理分区数据
	fmt.Println("-----插入数据到不同到分表-----")
	u, err := client.User.Create().SetAge(11).SetName("alice").SetDate(time.Now()).Save(ctx)
	if err != nil {
		log.Fatalf("failed creating users: %v", err)
	}
	februaryTime := time.Date(2024, time.February, 2, 23, 59, 59, 0, time.UTC)
	januaryTime := time.Date(2024, time.January, 2, 23, 59, 59, 0, time.UTC)
	u, err = client.User.Create().SetAge(22).SetName("alice").SetDate(februaryTime).Save(ctx)
	if err != nil {
		log.Fatalf("failed creating users: %v", err)
	}
	u, err = client.User.Create().SetAge(33).SetName("robot").SetDate(januaryTime).Save(ctx)
	if err != nil {
		log.Fatalf("failed creating users: %v", err)
	}
	fmt.Println("插入数据到不同到分表 成功", u)

	fmt.Println("-----更新数据到不同到分表-----")
	u, err = client.User.UpdateOneID(3).
		SetAge(33).
		Save(ctx)
	if err != nil {
		log.Fatalf("failed opening connection to postgres: %v", err)
	}
	fmt.Println("更新数据到不同到分表 成功", u)

	// 4。查询 
	s := time.Now()
	fmt.Println("-----按起止时间从不同到分表总查询数据-----")
	userPos, err := client.User.Query().Where(user.AgeGTE(22), user.DateGTE(startTime), user.DateLTE(endTime)).All(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("-----按起止时间从不同到分表总查询数据 成功", userPos)
	fmt.Println(userPos)

	fmt.Println("-----按起止时间\分页查询从不同到总查询数据-----")
	userPos2, err := client.User.Query().Where(user.AgeGTE(22), user.DateGTE(startTime), user.DateLTE(endTime)).
		Offset(5).Limit(int(10)).All(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("-----按起止时间\分页查询从不同到分表总查询数据 成功", userPos2)

	e := time.Now().Sub(s).Seconds()
	fmt.Println("end", e)

	ayalyzeSql := `explain analyze
SELECT *
FROM users
WHERE date > '2024-01-01' AND date < '2024-02-02';`

	r, err := db.Exec(ayalyzeSql)
	if err != nil {
		log.Println(ayalyzeSql, err)
	}
	fmt.Println("-----analyze 成功", r)
}

OUTPUT:

更新数据到不同到分表 成功 User(id=3, age=33, name=robot, date=Wed Jan  3 07:59:59 2024)
-----按起止时间从不同到分表总查询数据-----
 driver.Query: query=SELECT "users"."id", "users"."age", "users"."name", "users"."date" FROM "users" WHERE ("users"."age" >= $1 AND "users"."date" >= $2) AND "users"."date" <= $3 args=[22 2024-01-01 00:00:00 +0000 UTC 2024-03-30 23:59:59 +0000 UTC]
-----按起止时间从不同到分表总查询数据 成功 [User(id=3, age=33, name=robot, date=Wed Jan  3 07:59:59 2024) User(id=2, age=22, name=alice, date=Sat Feb  3 07:59:59 2024)]
[User(id=3, age=33, name=robot, date=Wed Jan  3 07:59:59 2024) User(id=2, age=22, name=alice, date=Sat Feb  3 07:59:59 2024)]
-----按起止时间\分页查询从不同到分表总查询数据-----
 driver.Query: query=SELECT "users"."id", "users"."age", "users"."name", "users"."date" FROM "users" WHERE ("users"."age" >= $1 AND "users"."date" >= $2) AND "users"."date" <= $3 LIMIT 10 OFFSET 5 args=[22 2024-01-01 00:00:00 +0000 UTC 2024-03-30 23:59:59 +0000 UTC]
-----按起起止时间\分页查询从不同到分表总查询数据 成功 []
end 0.006593416
-----analyze 成功 {0x14000320000 0}
Exiting.

通过和上一篇例子的代码的对比,不难发现,实现简单了,基本只需要自定义步骤1: 创建分区表和创建分区。其他的CURD安装标准的ent.io实现即可。对业务层面的侵入性几乎为零。

总结

本文介绍了在PostgreSQL数据库中实现分区的概念、优势和实现步骤。文中还通过重构上一篇的用户表的例子,演示ent.io实现分区的过程。总而言之,相对与分表,分区有更简易的实现,松耦合,更高的性能和更简化的数据管理。

参考

转载自:https://juejin.cn/post/7340865231380414473
评论
请登录