Баженова Анна
CREATE TABLE notes (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
text TEXT,
owner_id INTEGER,
CONSTRAINT fk_notes_users FOREIGN KEY (owner_id)
REFERENCES users (id)
);
CREATE TABLE notes (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
text TEXT,
owner_id INTEGER REFERENCES users (id)
);
ALTER TABLE notes
ADD CONSTRAINT fk_notes_users
FOREIGN KEY (owner_id)
REFERENCES users (id);
users | notes |
| |
id name | id name owner_id |
-- ------ | -- ----- -------- |
1 Олег | 1 Books 1 |
2 Сергей | 2 Films 1 |
3 Михаил | 3 Music 2 |
| 4 Rules NULL |
| 5 Markdown NULL |
SELECT users.id AS user_id,
users.name AS user_name,
notes.name AS note_name
FROM users
INNER JOIN notes
ON users.id = notes.owner_id;
users | notes |
| |
id name | id name owner_id |
-- ------ | -- ----- -------- |
1 Олег | 1 Books 1 |
2 Сергей | 2 Films 1 |
3 Михаил | 3 Music 2 |
| 4 Rules NULL |
| 5 Markdown NULL |
users | notes |
| |
id name | id name owner_id |
-- ------ | -- ----- -------- |
1 Олег | 1 Books 1 |
2 Сергей | 2 Films 1 |
3 Михаил | 3 Music 2 |
| 4 Rules NULL |
| 5 Markdown NULL |
user_id user_name note_name
------- --------- ---------
1 Олег Books
1 Олег Films
2 Сергей Music
SELECT users.id AS user_id,
users.name AS user_name,
notes.name AS note_name
FROM users
LEFT JOIN notes
ON users.id = notes.owner_id;
user_id user_name note_name
------- --------- ---------
1 Олег Books
1 Олег Films
2 Сергей Music
3 Михаил NULL
SELECT users.id AS user_id,
users.name AS user_name,
notes.name AS note_name
FROM users
RIGHT JOIN notes
ON users.id = notes.owner_id;
user_id user_name note_name
------- --------- ---------
1 Олег Books
1 Олег Films
2 Сергей Music
NULL NULL Rules
NULL NULL Markdown
SELECT users.id AS user_id,
users.name AS user_name,
notes.name AS note_name
FROM users
FULL JOIN notes
ON users.id = notes.owner_id;
user_id user_name note_name
------- --------- ---------
1 Олег Books
1 Олег Films
2 Сергей Music
3 Михаил NULL
NULL NULL Rules
NULL NULL Markdown
SELECT users.id AS user_id,
users.name AS user_name,
notes.name AS note_name
FROM users
CROSS JOIN notes;
SELECT users.id AS user_id,
users.name AS user_name,
notes.name AS note_name
FROM users, notes;
user_id user_name note_name
------- --------- ---------
1 Олег Books
1 Олег Films
1 Олег Music
1 Олег Rules
1 Олег Markdown
2 Сергей Books
2 Сергей Films
... ... ...
Выбираем самые важные колонки и переносим их:
CREATE INDEX user_name_idx ON users (name);
Создается по умолчанию для строк и чисел. Ускоряет следующие операции:
id = 1 (аналогично >, <, >=, <=)
id BETWEEN 1 AND 5
name LIKE 'ab%'
)
ORDER BY id
)Для них есть специальные типы индексов, которые поддерживают определенные операции над каждым типом.
WHERE tags @> ARRAY['javascript'];
WHERE point <@ BOX '(2,1),(7,4)'
CREATE INDEX users_lower_name ON users (lower(name));
CREATE INDEX users_data ON users (data ->> 'field');
SELECT * FROM notes
WHERE name = 'Books' AND owner_id = 1;
CREATE INDEX users_name_owner_id ON notes (name, owner_id);
Пример: индекс на тройку (x,y,z)
SELECT * FROM notes WHERE name = 'Unique book';
CREATE UNIQUE INDEX notes_name_idx ON notes (name);
Нюанс: NULL != NULL
Язык описания запросов - декларативный. Это означает, что мы описываем не то, как вычислить результат, а только то, каким он должен быть в терминах реляционной алгебры.
Но на самом деле запрос можно выполнить разными способами, дающими один результат, но разную производительность.
Как выбрать наилучший?
В PostgreSQL, как и в других SQL БД, есть планировщик запросов. На вход он получает SQL-запрос, а выдает наиболее оптимальный план выполнения
Какие факторы влияют на решение планировщика?
Заметили, что какой-то запрос очень долго работает. Как нам понять, почему?
EXPLAIN SELECT ...;
- выводит план запроса и предполагаемую "стоимость" выполнения
EXPLAIN ANALYZE SELECT ...;
- то же самое, но при этом выполняет запрос
и показывает время, затраченное на его выполнение
Практический пример
Есть база с миллионом пользователей и миллионом заметок, случайно распределенных между ними. Хотим получить пользователя с именем "Анна" и все ее заметки
EXPLAIN ANALYZE
SELECT id, name
FROM users
WHERE name = 'Анна';
Получаем следующий план запроса
Seq Scan on users (cost=20834)
Filter: ((name)::text = 'Анна'::text)
Rows Removed by Filter: 988699
Execution Time: 98.147 ms
Seq Scan
- мы полностью сканируем всю таблицу на диске (очень плохо)Rows Removed by Filter
- отфильтровали почти всех пользователейcost
- приблизительная оценка "стоимости" выполнения, вычисленная планировщикомExecution Time
- запрос выполнялся 98 миллисекундСоздадим индекс на поле name
CREATE INDEX users_name ON users (name);
Index Scan using users_name on users (cost=8.44)
Index Cond: ((name)::text = 'Анна'::text)
Execution Time: 0.048 ms
Index Scan using users_name
- использовали новый индексcost
= 8.44 (было 20834)Execution Time
= 0.048 мс (было 98 мс)Добавим в запрос заметки
EXPLAIN ANALYZE
SELECT u.name, n.name
FROM users u
JOIN notes n on n.owner_id = u.id
WHERE u.name = 'Анна';
План:
Hash Join
Hash Cond: (n.owner_id = u.id)
-> Seq Scan on notes n
-> Hash
-> Index Scan using users_name on users u
Index Cond: ((name)::text = 'Анна'::text)
Execution Time: 252.713 ms
Index Scan
- ищем пользователя по имени, используем индексHash
- для полученных пользователей строим хэш-таблицу по значениям idSeq Scan on notes
- сканируем все заметки (опять плохо)Hash Join
- для каждой заметки ищем в хэш-таблице пользователя по owner_id и соединяемExecution Time
= 252.713 мсДобавим индекс на поле owner_id
CREATE INDEX notes_owner_id ON notes (owner_id);
Nested Loop
-> Index Scan using users_name on users u
Index Cond: ((name)::text = 'Анна'::text)
-> Index Scan using notes_owner_id on notes n
Index Cond: (owner_id = u.id)
Execution Time: 0.054 ms
Nested Loop
- сначала находим пользователей, а затем для каждого из них в цикле ищем заметкиSeq Scan
Execution Time
= 0.054 мс (было 252.713 мс)Полезный сервис для визуализации плана запроса
npm install pg
npm install sequelize
npm install sequelize-typescript@canary
import { Sequelize, SequelizeOptions } from 'sequelize-typescript';
const sequelizeOptions: SequelizeOptions = {
// connection
host: 'localhost',
port: 5432,
username: 'user',
password: 'pass',
database: 'dbname',
// db options
dialect: 'postgres' // 'mysql', 'sqlite', 'mariadb', 'mssql'
};
const sequelize = new Sequelize(sequelizeOptions);
import { Model, Table } from 'sequelize-typescript';
@Table
class Note extends Model<Note> {}
@Table
class Note extends Model<Note> {
@Column(DataType.INTEGER)
id: number;
}
@Table
class Note extends Model<Note> {
@Column({
type: DataType.INTEGER,
field: 'owner_id'
})
ownerId: number;
}
PostgreSQL | Sequelize |
CHAR | CHAR |
VARCHAR(255) | STRING(255) |
TEXT | TEXT |
PostgreSQL | Sequelize |
INTEGER | INTEGER |
BIGINT | BIGINT |
REAL | REAL |
DOUBLE PRECISION | DOUBLE |
DECIMAL | DECIMAL |
PostgreSQL | Sequelize |
TIMESTAMP WITH TIME ZONE | DATE / NOW |
DATE | DATEONLY |
TIME | TIME |
PostgreSQL | Sequelize |
ARRAY | ARRAY |
JSON | JSON |
JSONB | JSONB |
@Table
class Note extends Model<Note> {
@AutoIncrement
@PrimaryKey
@Column(DataType.INTEGER)
id: number;
@AllowNull(false)
@Column(DataType.STRING)
name: string;
}
@Table
class Note extends Model<Note> {
@Column(DataType.STRING)
get name(): string {
return 'It is ' + this.getDataValue('name');
}
set name(value: string) {
const text = value.replace(/\*(\w+)\*/g, '$1');
this.setDataValue('name', text);
}
}
'films to *watch*' -> 'films to watch'
@Table
class Note extends Model<Note> {
@Is('Name', (value) => {
if (!/^[a-z]+$/i.test(value)) {
throw new Error(`Name contains invalid character!`);
}
})
@Column(DataType.STRING)
name: string;
}
@Table
class User extends Model<User> {
@Contains('@yandex.ru')
@IsEmail
@Column(DataType.STRING)
email: string;
}
@Table({
timestamps: true, // add 'created_at', 'updated_at'
paranoid: true, // add 'deleted_at'
underscored: true,
tableName: 'notes'
})
class Note extends Model<Note> {}
const sequelize = new Sequelize({
...
modelPaths: [__dirname + '/tables/user.ts']
});
sequelize.addModels([User, Category, Note]);
sequelize.addModels(['path/to/models']);
await sequelize.sync({ force: true }); // все таблицы
await Note.sync({ force: true }); // только конкретную
await Note.drop();
await Note.create({
name: 'Books',
text: 'Books to read'
});
await Note.bulkCreate([
{
name: 'Books',
text: 'Books to read',
ownerId: 3
},
{
name: 'Films',
text: 'Films to watch',
ownerId: 1
}
]);
const note = await Note.findOne({
where: {
name: 'Films'
}
});
const text = note.text;
const note = await Note.findOne({
where: {
name: 'Films'
},
attributes: ['id', 'text', ['name', 'title']]
});
import { Op } from 'sequelize';
const userModel = await User.findOne({
where: {
[Op.or]: [
{ name: { [Op.like]: 'А%' } },
{ saveDate: { [Op.gt]: '2018-04-09 13:25:13' } }
]
}
});
const note = await Note.findOne({
where: { id: 23 }
});
const note = await Note.findByPk(23);
const notes = await Note.findAll({
where: {
ownerId: 1
},
attributes: ['id', 'name']
});
const notes = await Note.findAll({
order: [
['name', 'DESC']
],
offset: 2,
limit: 2
});
const count = await Note.count({
where: {
ownerId: 1
}
});
const count = await Note.findAll({
attributes: [
'ownerId',
[
Sequelize.fn('count', Sequelize.col('name')),
'countNotes'
]
],
group: ['ownerId']
});
await Note.update(
{
text: 'My favorite books'
},
{
where: { name: 'Books' }
}
);
const note = await Note.findOne({
where: { name: 'Books' }
});
note.text = 'Important books to read';
await note.save();
await Note.destroy({
where: { id: 23 }
});
const note = await Note.findOne({
where: { id: 23 }
});
await note.destroy();
const note = await Note.findOrCreate({
where: { name: 'Books' },
defaults: {
name: 'Books',
text: 'Books to read',
ownerId: 1
}
});
class Note extends Model<Note> {
@ForeignKey(() => User)
@Column({
type: DataType.INTEGER,
field: 'owner_id'
})
ownerId: number;
}
class Note extends Model<Note> {
...
@BelongsTo(() => User)
user: User;
}
class User extends Model<User> {
...
@HasMany(() => Note)
notes: Note[];
}
class Note extends Model<Note> {
...
@HasOne(() => Category, 'best_note_id')
category: Category;
}
users | notes | categories
| |
id name | id name owner_id | id name best_note_id
-- ------ | -- ----- -------- | __ _____ ____________
1 Олег | 1 Books 1 | 1 study 1
2 Сергей | 2 Films 1 | 2 fun 3
3 Михаил | 3 Music 2 |
| 4 Rules NULL |
| 5 Markdown NULL |
await User.findAll({
attributes: ['name'],
include: [
{
model: Note,
attributes: ['name']
}
]
});
[
{
name: 'Олег',
notes: [
{ name: 'Films' },
{ name: 'Books' }
]
},
{
name: 'Сергей',
notes: [{ name: 'Music' }]
},
{
name: 'Михаил',
notes: []
}
]
await User.findAll({
attributes: ['name'],
include: [
{
model: Note,
where: {
name: {
[Op.or]: [
{ [Op.like]: 'F%' },
{ [Op.like]: 'M%' }
]
}
},
attributes: ['name']
}
]
});
[
{
name: 'Олег',
notes: [
{ name: 'Films' }
]
},
{
name: 'Сергей',
notes: [
{ name: 'Music' }
]
}
]
await User.findAll({
attributes: ['name'],
include: [
{
model: Note,
where: {
name: {
[Op.or]: [
{ [Op.like]: 'F%' },
{ [Op.like]: 'M%' }
]
}
},
attributes: ['name'],
required: false
}
]
});
[
{
name: 'Олег',
notes: [{ name: 'Films' }]
},
{
name: 'Сергей',
notes: [{ name: 'Music' }]
},
{
name: 'Михаил',
notes: []
}
]
const users = await User.findAll({
attributes: ['name'],
include: [
{
model: Note,
attributes: ['name'],
include: [
{
model: Category,
attributes: ['name']
}
]
}
]
});
[
{
name: 'Олег',
notes: [
{
name: 'Films',
category: null
},
{
name: 'Books',
category: {
name: 'study'
}
}
]
},
...
...
{
name: 'Сергей',
notes: [
{
name: 'Music',
category: {
name: 'fun'
}
}
]
},
{
name: 'Михаил',
notes: []
}
]
await sequelize.transaction(async function(t) {
await User.increment(
'account',
{ by: -50, where: { id: 1 }, transaction: t }
);
await User.increment(
'account',
{ by: 50, where: { id: 2 }, transaction: t }
);
});
npm install sequelize-cli
const path = require('path');
const DB_DIR = __dirname + '/db';
module.exports = {
'config': path.resolve(DB_DIR, 'config.js'),
'migrations-path': path.resolve(DB_DIR, 'migrations'),
'models-path': path.resolve(DB_DIR + 'tables'),
'seeders-path': path.resolve(DB_DIR, 'seeders')
};
migrations/1553446742822-add-user-column.ts
import { QueryInterface, DataTypes } from 'sequelize';
export = {
async up(queryInterface: QueryInterface) {
await queryInterface.addColumn('users', 'account', {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
});
},
async down(queryInterface: QueryInterface) {
await queryInterface.removeColumn('users', 'account');
}
};
npx sequelize db:migrate
// new table SequelizeMeta
npx sequelize db:migrate:undo
npx sequelize db:migrate:status