PostgreSQL power queries: TimescaleDB and PostgreSQL
TimescaleDB and PostgreSQL run the same files for this example: the SQL and the Go are identical.
Compare TimescaleDB withPostgreSQL
Configuration
sgl.yamlidentical
TimescaleDB
PostgreSQL
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: harbour4
- name: harbour5
dialect: postgres5
dialect: postgres6
migrations: db/migrations6
migrations: db/migrations7
queries: db/queries7
queries: db/queries8
go:8
go:9
package: store9
package: store10
out: internal/store10
out: internal/store11
driver: database/sql11
driver: database/sql12
migrate:12
migrate:13
dsn: ${env:DATABASE_URL}13
dsn: ${env:DATABASE_URL}Migrations
db/migrations/0001_harbour.up.sqlidentical
TimescaleDB
PostgreSQL
1
CREATE TABLE boats (1
CREATE TABLE boats (2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,3
name TEXT NOT NULL,3
name TEXT NOT NULL,4
gear JSONB NOT NULL DEFAULT '{}'4
gear JSONB NOT NULL DEFAULT '{}'5
);5
);6
6
7
CREATE TABLE landings (7
CREATE TABLE landings (8
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,8
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,9
boat_id BIGINT NOT NULL REFERENCES boats (id),9
boat_id BIGINT NOT NULL REFERENCES boats (id),10
species TEXT NOT NULL,10
species TEXT NOT NULL,11
kilos INTEGER NOT NULL,11
kilos INTEGER NOT NULL,12
landed_on DATE NOT NULL,12
landed_on DATE NOT NULL,13
notes TEXT NOT NULL,13
notes TEXT NOT NULL,14
search TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', species || ' ' || notes)) STORED14
search TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', species || ' ' || notes)) STORED15
);15
);16
16
17
CREATE INDEX landings_search_idx ON landings USING GIN (search);17
CREATE INDEX landings_search_idx ON landings USING GIN (search);18
18
19
CREATE TABLE landing_archive (19
CREATE TABLE landing_archive (20
id BIGINT PRIMARY KEY,20
id BIGINT PRIMARY KEY,21
boat_id BIGINT NOT NULL REFERENCES boats (id),21
boat_id BIGINT NOT NULL REFERENCES boats (id),22
species TEXT NOT NULL,22
species TEXT NOT NULL,23
kilos INTEGER NOT NULL,23
kilos INTEGER NOT NULL,24
landed_on DATE NOT NULL24
landed_on DATE NOT NULL25
);25
);db/migrations/0001_harbour.down.sqlidentical
TimescaleDB
PostgreSQL
1
DROP TABLE landing_archive;1
DROP TABLE landing_archive;2
DROP TABLE landings;2
DROP TABLE landings;3
DROP TABLE boats;3
DROP TABLE boats;Queries
AddBoatidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1
-- sgl.query(AddBoat, one)1
-- sgl.query(AddBoat, one)2
INSERT INTO boats (name, gear) VALUES ($1, $2) RETURNING id;2
INSERT INTO boats (name, gear) VALUES ($1, $2) RETURNING id;RecordLandingidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1
-- sgl.query(RecordLanding, exec)1
-- sgl.query(RecordLanding, exec)2
INSERT INTO landings (boat_id, species, kilos, landed_on, notes) VALUES ($1, $2, $3, $4, $5);2
INSERT INTO landings (boat_id, species, kilos, landed_on, notes) VALUES ($1, $2, $3, $4, $5);LatestLandingsidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1
-- sgl.query(LatestLandings, many)1
-- sgl.query(LatestLandings, many)2
SELECT DISTINCT ON (landings.boat_id) boats.name, landings.species, landings.landed_on2
SELECT DISTINCT ON (landings.boat_id) boats.name, landings.species, landings.landed_on3
FROM landings3
FROM landings4
JOIN boats ON boats.id = landings.boat_id4
JOIN boats ON boats.id = landings.boat_id5
ORDER BY landings.boat_id, landings.landed_on DESC, landings.id DESC;5
ORDER BY landings.boat_id, landings.landed_on DESC, landings.id DESC;BiggestLandingsidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1
-- sgl.query(BiggestLandings, many)1
-- sgl.query(BiggestLandings, many)2
SELECT boats.name, biggest.species, biggest.kilos2
SELECT boats.name, biggest.species, biggest.kilos3
FROM boats3
FROM boats4
LEFT JOIN LATERAL (4
LEFT JOIN LATERAL (5
SELECT species, kilos5
SELECT species, kilos6
FROM landings6
FROM landings7
WHERE landings.boat_id = boats.id7
WHERE landings.boat_id = boats.id8
ORDER BY kilos DESC8
ORDER BY kilos DESC9
LIMIT 29
LIMIT 210
) AS biggest ON true10
) AS biggest ON true11
ORDER BY boats.name, biggest.kilos DESC;11
ORDER BY boats.name, biggest.kilos DESC;SearchNotesidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1
-- sgl.query(SearchNotes, many)1
-- sgl.query(SearchNotes, many)2
SELECT landings.id, landings.notes, ts_rank(landings.search, websearch_to_tsquery('english', :terms)) AS rank2
SELECT landings.id, landings.notes, ts_rank(landings.search, websearch_to_tsquery('english', :terms)) AS rank3
FROM landings3
FROM landings4
WHERE landings.search @@ websearch_to_tsquery('english', :terms)4
WHERE landings.search @@ websearch_to_tsquery('english', :terms)5
ORDER BY rank DESC, landings.id;5
ORDER BY rank DESC, landings.id;BoatGearidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1
-- sgl.query(BoatGear, many)1
-- sgl.query(BoatGear, many)2
SELECT boats.name, gear.key, gear.value2
SELECT boats.name, gear.key, gear.value3
FROM boats3
FROM boats4
CROSS JOIN LATERAL jsonb_each(boats.gear) AS gear4
CROSS JOIN LATERAL jsonb_each(boats.gear) AS gear5
ORDER BY boats.name, gear.key;5
ORDER BY boats.name, gear.key;ArchiveLandingsidentical
TimescaleDB · db/queries/harbour.sql
PostgreSQL · db/queries/harbour.sql
1
-- sgl.query(ArchiveLandings, many)1
-- sgl.query(ArchiveLandings, many)2
WITH archived AS (2
WITH archived AS (3
DELETE FROM landings3
DELETE FROM landings4
WHERE landed_on < :before4
WHERE landed_on < :before5
RETURNING id, boat_id, species, kilos, landed_on5
RETURNING id, boat_id, species, kilos, landed_on6
)6
)7
INSERT INTO landing_archive (id, boat_id, species, kilos, landed_on)7
INSERT INTO landing_archive (id, boat_id, species, kilos, landed_on)8
SELECT id, boat_id, species, kilos, landed_on FROM archived8
SELECT id, boat_id, species, kilos, landed_on FROM archived9
RETURNING id, species, landed_on;9
RETURNING id, species, landed_on;What sgl generates
AddBoatidentical
TimescaleDB
PostgreSQL
1
func (queries *Queries) AddBoat(ctx context.Context, params AddBoatParams) (AddBoatRow, error)1
func (queries *Queries) AddBoat(ctx context.Context, params AddBoatParams) (AddBoatRow, error)2
2
3
type AddBoatParams struct {3
type AddBoatParams struct {4
Name string4
Name string5
Gear JSON5
Gear JSON6
}6
}7
7
8
type AddBoatRow struct {8
type AddBoatRow struct {9
ID int64 `json:"id"`9
ID int64 `json:"id"`10
}10
}11
11
12
type JSON []byte12
type JSON []byteRecordLandingidentical
TimescaleDB
PostgreSQL
1
func (queries *Queries) RecordLanding(ctx context.Context, params RecordLandingParams) error1
func (queries *Queries) RecordLanding(ctx context.Context, params RecordLandingParams) error2
2
3
type RecordLandingParams struct {3
type RecordLandingParams struct {4
BoatID int644
BoatID int645
Species string5
Species string6
Kilos int326
Kilos int327
LandedOn time.Time7
LandedOn time.Time8
Notes string8
Notes string9
}9
}LatestLandingsidentical
TimescaleDB
PostgreSQL
1
func (queries *Queries) LatestLandings(ctx context.Context) ([]LatestLandingsRow, error)1
func (queries *Queries) LatestLandings(ctx context.Context) ([]LatestLandingsRow, error)2
2
3
type LatestLandingsRow struct {3
type LatestLandingsRow struct {4
Name string `json:"name"`4
Name string `json:"name"`5
Species string `json:"species"`5
Species string `json:"species"`6
LandedOn time.Time `json:"landed_on"`6
LandedOn time.Time `json:"landed_on"`7
}7
}BiggestLandingsidentical
TimescaleDB
PostgreSQL
1
func (queries *Queries) BiggestLandings(ctx context.Context) ([]BiggestLandingsRow, error)1
func (queries *Queries) BiggestLandings(ctx context.Context) ([]BiggestLandingsRow, error)2
2
3
type BiggestLandingsRow struct {3
type BiggestLandingsRow struct {4
Name string `json:"name"`4
Name string `json:"name"`5
Species *string `json:"species"`5
Species *string `json:"species"`6
Kilos *int32 `json:"kilos"`6
Kilos *int32 `json:"kilos"`7
}7
}SearchNotesidentical
TimescaleDB
PostgreSQL
1
func (queries *Queries) SearchNotes(ctx context.Context, terms string) ([]SearchNotesRow, error)1
func (queries *Queries) SearchNotes(ctx context.Context, terms string) ([]SearchNotesRow, error)2
2
3
type SearchNotesRow struct {3
type SearchNotesRow struct {4
ID int64 `json:"id"`4
ID int64 `json:"id"`5
Notes string `json:"notes"`5
Notes string `json:"notes"`6
Rank *float32 `json:"rank"`6
Rank *float32 `json:"rank"`7
}7
}BoatGearidentical
TimescaleDB
PostgreSQL
1
func (queries *Queries) BoatGear(ctx context.Context) ([]BoatGearRow, error)1
func (queries *Queries) BoatGear(ctx context.Context) ([]BoatGearRow, error)2
2
3
type BoatGearRow struct {3
type BoatGearRow struct {4
Name string `json:"name"`4
Name string `json:"name"`5
Key string `json:"key"`5
Key string `json:"key"`6
Value JSON `json:"value"`6
Value JSON `json:"value"`7
}7
}8
8
9
type JSON []byte9
type JSON []byteArchiveLandingsidentical
TimescaleDB
PostgreSQL
1
func (queries *Queries) ArchiveLandings(ctx context.Context, before time.Time) ([]ArchiveLandingsRow, error)1
func (queries *Queries) ArchiveLandings(ctx context.Context, before time.Time) ([]ArchiveLandingsRow, error)2
2
3
type ArchiveLandingsRow struct {3
type ArchiveLandingsRow struct {4
ID int64 `json:"id"`4
ID int64 `json:"id"`5
Species string `json:"species"`5
Species string `json:"species"`6
LandedOn time.Time `json:"landed_on"`6
LandedOn time.Time `json:"landed_on"`7
}7
}The program
example.goidentical
TimescaleDB
PostgreSQL
1
package main1
package main2
2
3
import (3
import (4
"cmp"4
"cmp"5
"context"5
"context"6
"database/sql"6
"database/sql"7
"fmt"7
"fmt"8
"io"8
"io"9
"slices"9
"slices"10
"time"10
"time"11
11
12
"seagull.sh/seagull/examples/postgres/031_postgres_power_queries/internal/store"12
"seagull.sh/seagull/examples/postgres/031_postgres_power_queries/internal/store"13
)13
)14
14
15
// run records a week of landings at a small harbour and reads them back with five queries15
// run records a week of landings at a small harbour and reads them back with five queries16
// that lean on PostgreSQL's own SQL. Seagull types each of them:16
// that lean on PostgreSQL's own SQL. Seagull types each of them:17
//17
//18
// - LatestLandings uses DISTINCT ON to keep the first row of each boat in ORDER BY18
// - LatestLandings uses DISTINCT ON to keep the first row of each boat in ORDER BY19
// order, which is its latest landing;19
// order, which is its latest landing;20
// - BiggestLandings runs a LIMIT 2 subquery once per boat with LEFT JOIN LATERAL, so20
// - BiggestLandings runs a LIMIT 2 subquery once per boat with LEFT JOIN LATERAL, so21
// the subquery can see boats.id; a boat with no landings still appears, which makes21
// the subquery can see boats.id; a boat with no landings still appears, which makes22
// the subquery's columns pointers;22
// the subquery's columns pointers;23
// - SearchNotes matches a generated tsvector column against websearch_to_tsquery, which23
// - SearchNotes matches a generated tsvector column against websearch_to_tsquery, which24
// takes the syntax people type into search boxes, and ranks with ts_rank;24
// takes the syntax people type into search boxes, and ranks with ts_rank;25
// - BoatGear expands each boat's JSONB gear into key and value rows with jsonb_each, a25
// - BoatGear expands each boat's JSONB gear into key and value rows with jsonb_each, a26
// function used as a table; value stays store.JSON;26
// function used as a table; value stays store.JSON;27
// - ArchiveLandings moves old landings in one statement: the DELETE inside the CTE27
// - ArchiveLandings moves old landings in one statement: the DELETE inside the CTE28
// hands its RETURNING rows to the INSERT, and the INSERT's RETURNING comes back.28
// hands its RETURNING rows to the INSERT, and the INSERT's RETURNING comes back.29
//29
//30
// Takes database (*sql.DB) which is a migrated database.30
// Takes database (*sql.DB) which is a migrated database.31
// Takes output (io.Writer) which receives each query's result.31
// Takes output (io.Writer) which receives each query's result.32
//32
//33
// Returns error when a query fails.33
// Returns error when a query fails.34
func run(ctx context.Context, database *sql.DB, output io.Writer) error {34
func run(ctx context.Context, database *sql.DB, output io.Writer) error {35
queries := store.New(database)35
queries := store.New(database)36
36
37
if err := recordWeek(ctx, queries); err != nil {37
if err := recordWeek(ctx, queries); err != nil {38
return err38
return err39
}39
}40
40
41
latest, err := queries.LatestLandings(ctx)41
latest, err := queries.LatestLandings(ctx)42
if err != nil {42
if err != nil {43
return fmt.Errorf("finding the latest landings: %w", err)43
return fmt.Errorf("finding the latest landings: %w", err)44
}44
}45
fmt.Fprintln(output, "latest landing per boat (DISTINCT ON):")45
fmt.Fprintln(output, "latest landing per boat (DISTINCT ON):")46
for _, landing := range latest {46
for _, landing := range latest {47
fmt.Fprintf(output, " %-9s %-8s %s\n", landing.Name, landing.Species, landing.LandedOn.Format(time.DateOnly))47
fmt.Fprintf(output, " %-9s %-8s %s\n", landing.Name, landing.Species, landing.LandedOn.Format(time.DateOnly))48
}48
}49
49
50
biggest, err := queries.BiggestLandings(ctx)50
biggest, err := queries.BiggestLandings(ctx)51
if err != nil {51
if err != nil {52
return fmt.Errorf("finding the biggest landings: %w", err)52
return fmt.Errorf("finding the biggest landings: %w", err)53
}53
}54
fmt.Fprintln(output, "two biggest landings per boat (LEFT JOIN LATERAL):")54
fmt.Fprintln(output, "two biggest landings per boat (LEFT JOIN LATERAL):")55
for _, landing := range biggest {55
for _, landing := range biggest {56
if landing.Species == nil || landing.Kilos == nil {56
if landing.Species == nil || landing.Kilos == nil {57
fmt.Fprintf(output, " %-9s nothing landed\n", landing.Name)57
fmt.Fprintf(output, " %-9s nothing landed\n", landing.Name)58
continue58
continue59
}59
}60
fmt.Fprintf(output, " %-9s %-8s %3dkg\n", landing.Name, *landing.Species, *landing.Kilos)60
fmt.Fprintf(output, " %-9s %-8s %3dkg\n", landing.Name, *landing.Species, *landing.Kilos)61
}61
}62
62
63
for _, terms := range []string{"mackerel", "crab or pollock"} {63
for _, terms := range []string{"mackerel", "crab or pollock"} {64
found, err := queries.SearchNotes(ctx, terms)64
found, err := queries.SearchNotes(ctx, terms)65
if err != nil {65
if err != nil {66
return fmt.Errorf("searching for %q: %w", terms, err)66
return fmt.Errorf("searching for %q: %w", terms, err)67
}67
}68
fmt.Fprintf(output, "search %q (full-text):\n", terms)68
fmt.Fprintf(output, "search %q (full-text):\n", terms)69
for _, match := range found {69
for _, match := range found {70
fmt.Fprintf(output, " #%d rank %s %s\n", match.ID, formatRank(match.Rank), match.Notes)70
fmt.Fprintf(output, " #%d rank %s %s\n", match.ID, formatRank(match.Rank), match.Notes)71
}71
}72
}72
}73
73
74
gear, err := queries.BoatGear(ctx)74
gear, err := queries.BoatGear(ctx)75
if err != nil {75
if err != nil {76
return fmt.Errorf("listing gear: %w", err)76
return fmt.Errorf("listing gear: %w", err)77
}77
}78
fmt.Fprintln(output, "gear (jsonb_each):")78
fmt.Fprintln(output, "gear (jsonb_each):")79
for _, item := range gear {79
for _, item := range gear {80
fmt.Fprintf(output, " %-9s %-5s %s\n", item.Name, item.Key, item.Value)80
fmt.Fprintf(output, " %-9s %-5s %s\n", item.Name, item.Key, item.Value)81
}81
}82
82
83
cutoff := july(3)83
cutoff := july(3)84
archived, err := queries.ArchiveLandings(ctx, cutoff)84
archived, err := queries.ArchiveLandings(ctx, cutoff)85
if err != nil {85
if err != nil {86
return fmt.Errorf("archiving landings: %w", err)86
return fmt.Errorf("archiving landings: %w", err)87
}87
}88
// RETURNING rows come back in no promised order, so sort them.88
// RETURNING rows come back in no promised order, so sort them.89
slices.SortFunc(archived, func(a, b store.ArchiveLandingsRow) int { return cmp.Compare(a.ID, b.ID) })89
slices.SortFunc(archived, func(a, b store.ArchiveLandingsRow) int { return cmp.Compare(a.ID, b.ID) })90
fmt.Fprintf(output, "archived before %s (DELETE and INSERT in one statement):\n", cutoff.Format(time.DateOnly))90
fmt.Fprintf(output, "archived before %s (DELETE and INSERT in one statement):\n", cutoff.Format(time.DateOnly))91
for _, landing := range archived {91
for _, landing := range archived {92
fmt.Fprintf(output, " #%d %-8s %s\n", landing.ID, landing.Species, landing.LandedOn.Format(time.DateOnly))92
fmt.Fprintf(output, " #%d %-8s %s\n", landing.ID, landing.Species, landing.LandedOn.Format(time.DateOnly))93
}93
}94
return nil94
return nil95
}95
}96
96
97
// recordWeek adds three boats and five landings.97
// recordWeek adds three boats and five landings.98
//98
//99
// Takes queries (*store.Queries) which add the rows.99
// Takes queries (*store.Queries) which add the rows.100
//100
//101
// Returns error when a row cannot be added.101
// Returns error when a row cannot be added.102
func recordWeek(ctx context.Context, queries *store.Queries) error {102
func recordWeek(ctx context.Context, queries *store.Queries) error {103
boats := []store.AddBoatParams{103
boats := []store.AddBoatParams{104
{Name: "Kittiwake", Gear: store.JSON(`{"nets": 2, "pots": 40}`)},104
{Name: "Kittiwake", Gear: store.JSON(`{"nets": 2, "pots": 40}`)},105
{Name: "Puffin", Gear: store.JSON(`{"lines": 12}`)},105
{Name: "Puffin", Gear: store.JSON(`{"lines": 12}`)},106
{Name: "Gannet", Gear: store.JSON(`{}`)},106
{Name: "Gannet", Gear: store.JSON(`{}`)},107
}107
}108
ids := make(map[string]int64, len(boats))108
ids := make(map[string]int64, len(boats))109
for _, boat := range boats {109
for _, boat := range boats {110
added, err := queries.AddBoat(ctx, boat)110
added, err := queries.AddBoat(ctx, boat)111
if err != nil {111
if err != nil {112
return fmt.Errorf("adding %s: %w", boat.Name, err)112
return fmt.Errorf("adding %s: %w", boat.Name, err)113
}113
}114
ids[boat.Name] = added.ID114
ids[boat.Name] = added.ID115
}115
}116
landings := []store.RecordLandingParams{116
landings := []store.RecordLandingParams{117
{BoatID: ids["Kittiwake"], Species: "mackerel", Kilos: 120, LandedOn: july(1), Notes: "a big shoal of mackerel off the point"},117
{BoatID: ids["Kittiwake"], Species: "mackerel", Kilos: 120, LandedOn: july(1), Notes: "a big shoal of mackerel off the point"},118
{BoatID: ids["Kittiwake"], Species: "crab", Kilos: 35, LandedOn: july(2), Notes: "pots hauled in a heavy swell"},118
{BoatID: ids["Kittiwake"], Species: "crab", Kilos: 35, LandedOn: july(2), Notes: "pots hauled in a heavy swell"},119
{BoatID: ids["Puffin"], Species: "pollock", Kilos: 60, LandedOn: july(1), Notes: "lines over the wrecks"},119
{BoatID: ids["Puffin"], Species: "pollock", Kilos: 60, LandedOn: july(1), Notes: "lines over the wrecks"},120
{BoatID: ids["Kittiwake"], Species: "mackerel", Kilos: 80, LandedOn: july(4), Notes: "feathers out, gulls everywhere"},120
{BoatID: ids["Kittiwake"], Species: "mackerel", Kilos: 80, LandedOn: july(4), Notes: "feathers out, gulls everywhere"},121
{BoatID: ids["Puffin"], Species: "mackerel", Kilos: 25, LandedOn: july(3), Notes: "a few mackerel by the harbour wall"},121
{BoatID: ids["Puffin"], Species: "mackerel", Kilos: 25, LandedOn: july(3), Notes: "a few mackerel by the harbour wall"},122
}122
}123
for _, landing := range landings {123
for _, landing := range landings {124
if err := queries.RecordLanding(ctx, landing); err != nil {124
if err := queries.RecordLanding(ctx, landing); err != nil {125
return fmt.Errorf("recording %s: %w", landing.Species, err)125
return fmt.Errorf("recording %s: %w", landing.Species, err)126
}126
}127
}127
}128
return nil128
return nil129
}129
}130
130
131
// july returns a day in July 2026, the week the landings happen in.131
// july returns a day in July 2026, the week the landings happen in.132
//132
//133
// Takes day (int) which is the day of the month.133
// Takes day (int) which is the day of the month.134
//134
//135
// Returns time.Time which is midnight UTC on that day, as a DATE column holds it.135
// Returns time.Time which is midnight UTC on that day, as a DATE column holds it.136
func july(day int) time.Time {136
func july(day int) time.Time {137
return time.Date(2026, time.July, day, 0, 0, 0, 0, time.UTC)137
return time.Date(2026, time.July, day, 0, 0, 0, 0, time.UTC)138
}138
}139
139
140
// formatRank formats a ts_rank score to four places, or returns NULL when it is nil.140
// formatRank formats a ts_rank score to four places, or returns NULL when it is nil.141
//141
//142
// Takes rank (*float32) which is the score.142
// Takes rank (*float32) which is the score.143
//143
//144
// Returns string which is the text to print.144
// Returns string which is the text to print.145
func formatRank(rank *float32) string {145
func formatRank(rank *float32) string {146
if rank == nil {146
if rank == nil {147
return "NULL"147
return "NULL"148
}148
}149
return fmt.Sprintf("%.4f", *rank)149
return fmt.Sprintf("%.4f", *rank)150
}150
}