Базы данных: введение, часть пятая

Илья Тетерин
2012-10-22

(use arrow keys or PgUp/PgDown to move slides)

Мне нужно N рядов из базы...

Дай мне N рядов из Memcache

Memcache - распределенный (шардированный) key-value storage.

Клиент знает на какие ноды положены какие ключи.

Ой, узнать существующие ключи невозможно.

Долго? Нет, очень быстро, ибо вообще невозможно.

Как жить?

Клиентское приложение должно знать ключи, других вариантов нет.

А у меня есть список ключей!

Ок, запускаем цикл, перебираем ключи, делаем запросы в Memcache за каждым.

Ой, ключ есть, а значения нет ... ой, опять - ключ есть, значения нет.

Хм, никто же не обещал, что по ключу найдется - таймаут < 30 дней и могли "выпихнуть", если памяти мало.

Сценарий исполнения запроса get key = 7

Помните с прошлой лекции?

Не быстро как-то для 100 000 ключей ...

Дай мне N рядов из Redis

redis - key-value стораж (не шардированный).

Ура! Есть команда KEYS <pattern> - возвращает все ключи, попадающие под паттерн.

Сложность - O(N), где N количество ключей.

Редис берет каждый ключ, проверяет через "подходит под регекс", отдает нам.

Собираем ключи в список у себя.

Дальше для каждого полученного ключа делаем запрос в Redis "Дай значение для ключа X."

Здесь сложность - O(1), правда "стоимость" каждого запроса != 0 - см. предыдущий слайд и предыдущая лекция.

А что, если?

A: ... пока я обходил ключи добавили новый ключ в Redis?

... а я его не увижу (коллекция ключей уже на стороне клиента - отсоединена от сервера) ...

Б: ... пока я обходил ключи удалили значение ...

Я полезу в Redis "дай", а он мне скажет "нет такого".

Похоже отсутствует изоляция одних пользователей от других ...

Для счастья нужна атомарность, согласованность и изолированность (ACI[d]) .

А у меня сейчас: не атомарно (отдельно ключи, отдельно значения - разными запросами), несогласовано (ключ был секунду назад, а значения _уже_ нет), не изолировано (я работаю, а кто-то мне мешает).

В: ... а если у меня клиент "шардирует" данные на M Редисов?

Тогда пишем цикл - возьми с первого редиса, возьми со второго ... на стороне клиента и обрабатываем полученные данные когда обошли все редисы.

А что, если мне надо не по ключу, а по полю?

Дай мне всех, у кого фамилия Иванов.

Идем в REDIS.

Получаем оттуда список всех ключей.

Итератор по всем ключам.

Дай мне запись из Redis.

В этой записи фамилия = Иванов?

Если нет, то откинуть

Если да, "отложить в сторону"

Давай следующую запись.

Кончились ключи?

Теперь обработаем отложенное ...

А давайте засунем цикл на сервер, в базу?

На стороне базы опишем абстрактный цикл по всем рядам, принимающий в себя фильтр.

С клиента будем передавать "фильтр", который надо применить к каждому ряду.

Если фильтр сказал "ДА", откладываем в сторону.

Все отложенные - возвращаем на клиента.

Счастье!

Ой, а как это описать фильтр на клиенте и передать на сервер? Это что, кусок кода туда отправить??? Мы же раньше только данные отправляли.

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

Ой, нам же больше не подходит key-value ... а как хорошо было хранить key -> byte[] ...

Я слышал есть document - oriented databases

Document oriented база данных знает о структуре хранимых данных.

Существует возможность получать "подмножество" документа - кусок документа.

Есть возможность искать по содержимому документа.

Яркий пример - MongoDB by 10gen.

Как поставить?

Существует brew для Mac: https://github.com/mxcl/homebrew (аналог dpkg из Ubuntu)
Как поставить?
$ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)"

// обновить список пакетов 
brew update

// поставить готовый пакет mongodb
brew install mongodb

// запустить сервер
mongod

// запустить клиент и зайти в базу
mongo

Сходим в Монгу руками...

pulser-osx:~ pulser$ mongo 
MongoDB shell version: 2.0.5
connecting to: test
> help
    db.help()                    help on db methods
    db.foo.find()                list objects in collection foo
    db.foo.find( { a : 1 } )     list objects in foo where a == 1
> use mydb
switched to db mydb
> show collections; // пусто, однако
> j = { name : "pulser", twitter : "@ya_pulser" }
{ "name" : "pulser", "twitter" : "@ya_pulser" }
> db.persons.save(j);
> db.persons.find()
{ "_id" : ObjectId("508...7a4"), "name" : "pulser", "twitter" : "@ya_pulser" }

> db.persons.save( { name: "ivanov", twitter: "@ivanov" } )
> db.persons.save( { name: "ivanov", twitter: "@ivanov2" } )
> db.persons.find()
{ "_id" : ObjectId("508...7a4"), "name" : "pulser", "twitter" : "@ya_pulser" }
{ "_id" : ObjectId("508...7a5"), "name" : "ivanov", "twitter" : "@ivanov" }
{ "_id" : ObjectId("508...7a6"), "name" : "ivanov", "twitter" : "@ivanov2" }

> db.persons.find({name:"ivanov"})
{ "_id" : ObjectId("508...7a5"), "name" : "ivanov", "twitter" : "@ivanov" }
{ "_id" : ObjectId("508...7a6"), "name" : "ivanov", "twitter" : "@ivanov2" }

А теперь из Java в Монгу...

// https://github.com/mongodb/mongo-java-driver/downloads - mongo-2.9.1.jar 
import com.mongodb.*;

// соединимся с базой
final Mongo mongo = new Mongo("localhost", 27017);
final DB db = mongo.getDB("mydb");

// сформулируем запрос - что хотим
final BasicDBObject query = new BasicDBObject();
query.put("name", "ivanov");
// откуда хотим
final DBCollection coll = db.getCollection("persons");

// исполним запрос, получим в буфер 
final DBCursor cursor = coll.find(query);
// пробежимся по всем записям
while (cursor.hasNext()) {
    final DBObject item = cursor.next();
    System.out.println(item);
}
// освободим буфера
cursor.close();
// отпустим базу
mongo.close();

{ "_id" : ObjectId("508...7a5"), "name" : "ivanov", "twitter" : "@ivanov" }
{ "_id" : ObjectId("508...7a6"), "name" : "ivanov", "twitter" : "@ivanov2" }

Мы заставили базу работать ... :)

На клиенте мы сформулировали запрос в форме фильтра.

Отправили его на сервер.

Там он применился ко всем записям.

Мы получили на клиента только те записи, что нам надо.

Ура! Сервер работает на нас.

Что круто?

Гибкая схема - какие поля захотел, те и засунул.

Что смущает?

Документ велик и хранит много информации.

Описан в bson/json - может отъедать много памяти на сервере.

Список полей неизвестен - будем перебирать документы в которых вообще нету полей.

Будет медленно, прожорливо и небыстро ...

Реляционная база данных

Mongo vs MySQL прямо как Javascript/Python vs Java ...

Монго - гибкая схема, что хочу, то и кладу.

MySQL - сначала я задам типы, потом они будут проверятся, всегда всё четко.

Каждый тип - в свою таблицу.

{ "_id" : ObjectId("508...7a5"), "name" : "ivanov", "twitter" : "@ivanov" }

CREATE TABLE persons ( 
  id integer not null primary key, -- обязательный ключ
  name varchar(255) not null,      -- обязательно имя длиной до 255 символов
  twitter: varchar(255));          -- опциональный твиттер 255 символов
pulser = {}
vs
class Person {
   final Integer id; // обязательный ключ
   @NotNull String name; 
   @Nullable String twitter;
   public Person ( final int id, final String name ) {
     this.id = id;
     this.name = name;
   } } 

MySQL из консольного клиента.

root@nas:~# mysql
Welcome to the MySQL monitor.  Commands end with ; or \g.
Server version: 5.1.61-0ubuntu0.11.10.1 (Ubuntu)
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> create database mydb;
Query OK, 1 row affected (0.08 sec)
mysql> use mydb;
Database changed
mysql> create table persons ( id integer primary key, 
   name varchar(255) not null, twitter varchar(255));

mysql> insert into persons (id, name) values (7, 'pulser');
mysql> insert into persons (id, name, twitter) values (9, 'ivanov', '@ivanov');
mysql> insert into persons values (10, 'ivanov', '@ivanov2');

mysql> select * from persons where name = 'ivanov';
+----+--------+----------+
| id | name   | twitter  |
+----+--------+----------+
|  9 | ivanov | @ivanov  |
| 10 | ivanov | @ivanov2 |
+----+--------+----------+
2 rows in set (0.00 sec)

mysql> create user myreader identified by 'mypassword';
mysql> grant all on mydb.* to myreader identified by 'mypassword';

MySQL из Java...

Кстати: How Do I Enable Remote Access To MySQL Database Server?

import java.sql.*;

// загрузим драйвер, он сам где надо пропишется
Class.forName("com.mysql.jdbc.Driver"); // mysql-connector-java-5.1.22.tar.gz
// соединение с базой
Connection conn = DriverManager
    .getConnection("jdbc:mysql://nas.lan/mydb?" + 
        "user=myreader&password=mypassword");

// готовим запрос (резервируем память)
Statement st = conn.createStatement();
// исполняем запрос и получаем "ответ" в форме rs
ResultSet rs = st
        .executeQuery("select * from persons where name = 'ivanov'");
// пока есть ряды
while (rs.next()) {
    // выводим колонки
    System.out.println("id: " + rs.getInt(1) + ", name: " +
            rs.getString(2) + ", twitter: " + rs.getString(3));
}
// закрываем буфера
rs.close(); st.close();
// закрываем соединение
conn.close();

Усложним задачу: Mongo со странами ...

Хочу хранить имя, твиттер, компанию.

> db.persons.drop()
true
> show collections;
system.indexes

> db.persons.save ({name:"pusler",twitter:"@ya_pulser",company:"Yandeks"})
> db.persons.save ({name:"lilosea",twitter:"@lilosea",company:"Yandex"})
> db.persons.save ({name:"ivanov",twitter:"@ivanov",Company:"Yandex"})
> db.persons.find()
{ "_id" : ObjectId("508..c28"), "name" : "pusler".."company" : "Yandeks" }
{ "_id" : ObjectId("508..c29"), "name" : "lilosea".."company" : "Yandex" }
{ "_id" : ObjectId("508..c2a"), "name" : "ivanov".."Company" : "Yandex" }

> db.persons.find({company:"Yandex"})
{ "_id" : ObjectId("508..c29"), "name" : "lilosea".."company" : "Yandex" }

Траблы - опечатки! В названиях атрибутов, в значениях атрибутов. Нехорошо!

Нам поможет реляционная модель. В ней жесткие типы, в ней фиксированная модель - не будет опечаток в названиях атрибутов. В реляционной модели требуют, дабы данные были один раз - wiki:Нормальная форма (6 форм + 2 экстра-формы) - не будет опечаток в значениях.

MySQL версия со странами ...

mysql> drop table persons;
mysql> create table companies ( id integer not null primary key,
    name varchar(255) not null );
mysql> insert into companies values (7, 'Yandex');

mysql> create table persons ( id integer not null primary key, 
    name varchar(255) not null, 
    twitter varchar(255), 
    company_id integer, 
    FOREIGN KEY (company_id) REFERENCES companies(id));

mysql> insert into persons values (7, 'pulser', '@pulser', 7);
mysql> insert into persons values (3, 'dr.who', null, null );
mysql> select * from persons;
| id | name   | twitter | company_id |
+----+--------+---------+------------+
|  7 | pulser | @pulser |          7 |
|  3 | dr.who | NULL    |       NULL |
2 rows in set (0.00 sec)

mysql> select persons.name, companies.name 
from persons left outer join companies on persons.company_id = companies.id;
| name   | name   |
+--------+--------+
| pulser | Yandex |
| dr.who | NULL   |
2 rows in set (0.01 sec)

А что такое join? Что такое left outer join? А зачем?

Внутри document-oriented хранится целиком документ и его можно сразу выдавать клиенту.

Внутри RDBMS хранятся "составляющие" документа. Перед выдачей их надо "собрать" aka произвести "join".

Чем более жестко описана структура - тем больше будет "составляющих", тем сложнее процесс сборки.

Обычный join ( select * from x join y on x.v = y.v ) требует, чтобы данные, удовлетворяющие требованию соединения, были в обеих таблицах.

Если соответвующих рядов нет - нет ряда в множестве, возвращаемом клиенту.

Outer join позволяет вернуть все ряды из одной таблицы, и подставить к ее рядам ряды из другой в случае, если такие есть. ( select * from x left outer join y on x.v = y.v -- вернет все ряды из x).

Мне нужно N рядов из базы... почему долго?!

Сценарий исполнения запроса

почти ничем не отличается от get(7)

получает данные из коллекции - а как?

Индекс - это структура в базе, которая по значению может выдать точный указатель на ряд (rowid).

Понятие ROWID:
* указатель на ряд данных

Понятие "стоимость" запроса:
* сколько ресурсов нужно на получение информации
* сколько времени это займет (оценочно)

Oracle ROWID (url)

В индексе хранятся rowid. Они сильно похожи на указатель в памяти, но указывающие на данные внутри базы (во внешнюю память).

The Oracle 8 format is on 10 bytes:

bits 1 to 32 (bytes 1 to 4): 
  data object id (0-4294967295) // таблица, 2^32

bits 33 to 44 (byte 5 and half byte 6): 
  file number inside the tablespace (0-4095) // 2^12
  // вся файловая система побита на кусочки - tablespace

bits 45 to 64 (half byte 6 and bytes 7 and 8): 
  block number inside the file (0-1048575) // 2^20
  // внутри tablespace живут файлы

bits 65 to 80 (bytes 9 and 10): 
  row number inside the block (0-65535) // 2^16

select rowid from persons where name like '%pulser%'
AAATCsAAGAAAAH5AAn -- 18 hex digits
  AAATCs - объект (таблица users)
  AAG    - относительный номер файла в tablespace
  AAAAH5 - номер блока в файле
  AAn    - номер ряда таблицы в блоке

создать индекс

Oracle / MySQL / MongoDB - ссылки на синтаксис в документации

create table city ( 
  id number primary key, 
  name varchar2(50), 
  state varchar2(50),
  country varchar2(50), 
  yandex_office number(1),
  start_ts timestamp);

insert into city ( 7, 'Москва', 'ЦФО', 'Россия', 1, systimestamp );
insert into city ( 8, 'Санкт-Петербург', 'СЗФО', 'Россия', 1, systimestamp );
insert into city ( 9, 'Тверь', 'ЦФО', 'Россия', 0, null );

-- уникальный индекс - повторов названий нет
create unique index idx_city_name on city (name);

-- простой индекс - допускает повторы
create index idx_city_country on city(country);
create index idx_city_ts on city (start_ts);

-- битовые маски по значению 
-- (nb: если значений много => много масок - плохо)
create bitmap index idx_city_office on city(yandex_office);

-- составной индекс по двум полям
create index idx_compund on city(country, state);

index scan

Что такое сортировка? Просим базу вернуть ряды в нужном порядке на клиента.

select * from persons order by name; -- SQL

db.persons.find().sort({twitter:1}) -- mongo

Стоимость

Oracle генерирует все возможные "пути" исполнения запроса.

Будем использовать индекс?

Первой возьмем страну или людей? етс.

Критерии:

Для каждого строится оценка стоимости: cost = X * колво блоков чтения + Y * колво позиционирования + W * колво данных к сортировке.

Выбирает более дешевый и кеширует его.

Точную формулу не знаю и планы сравниваю "в относительных единицах".

План выбран и готов к исполнению

Делаем буфер: размер ряда * количество рядов на сервере

Буфер под данные - в памяти ? на диске ? А в /tmp/mysql заглядывали?

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

Snapshot too old / другие транзакции - слишком долго читали, исходные данные уже переписаны и недоступны (SCN, циклический undo log).

Маленькие таблицы можно Oracle (ссылка) просить удерживать в памяти

create table x ( 
  id number (20) primary key, 
  value varchar2(128)) 
  cache;

Получение данных на клиенте

Размер ряда - мета информация, доступная после parse запроса на сервере

Количество рядов в буфере - настройка клиента JDBC - statement.setFetchSize(N рядов)

размер ряда * колво рядов => OutOfMemory

Прочитали буфер - идем опять по сети "дай следующую пачку". 50 мс на заход, а если у нас 100 000 рядов по 10 в пачке?

По умолчанию: в Оракле 10 рядов, что очень мало; а в MySQL - отдай все ряды за один заход - очень много, здравствуй OOM.

MongoDB - можно установить колво рядов, но буфер <= 4Mb

Буфер делается под макс. возможный размер. Следовательно таблица с 10 varchar(4000) полей = 40Кб на ряд = много памяти

Обратите внимание на fetchSize - это важно ... и самое простое, что дает "вау, как быстро стало".

Blog пост про MySQL MySQL JDBC Memory Usage on Large ResultSet

google:useCursorFetch mysql jdbc

Далее: explain plan запроса в базе

Все базы строят план исполнения запроса

Для каждого запроса можно посмотреть план

Некоторые дают влиять на план - Oracle: select /*+ ... */ - hints

Переформулируя план - находим-теряем порядки по скорости работы

wikipedia:Query_Plan / wikipedia:План_выполнения_запроса

google:explain plan ИМЯ_БАЗЫ - ищите!

Найдите это для своей базы - и смотрите запросы.

Понимание плана запроса - это 90%+ успеха в скорости.

PSOUG: Oracle Hints

Но дабы понимать план - надо понимать детали железа - скорость и объем памяти, скорость диска и позиционирования етс. Единственный путь - практика и опыт.

distinct / union / order by / group by

select distinct ... 

select ...  union select ...

select ... order by 

select ... group by

Требуют сортировки ...

Разные алгоритмы, но всегда буфер, часто большой

Не влезло в память - сортируем на диске (o_O) - на порядки медленнее

wiki: External sorting

MySQL: как сортирует(filesort), docs

Oracle: Inside Oracle Sorting, google:order by sort area, ...

join / left join / right join

Join'ы это комбинация доступа по ключу и за несколькими рядами прямо в одном запросе.

Смотрите explain plan, думайте ... и пробуйте добавил-убрал индекс, хинт, таблицу

Oracle документация: 11.6 Understanding Joins

* Nested Loop Joins
* Hash Joins
* Sort Merge Joins
* Cartesian Joins
* Outer Joins

Nested Loops:
берем ряд из одной таблицы - и ищем ему соответствие в другой
потом следующий ряд из первой ... и ищем во второй... -> O(n*m)

Merge sort join
* сортируем ряды обоих таблиц по ключу объединения - o(m ln m) + o (n ln n)
* "сливаем" два отсортированных списка -> o(max(n,m)) -> O(n)

NoSQL и join???

NoSQL: we do NOT do joins ...

ибо: объемы, шардирование, партиционирование, много нод с данными, сеть, распределенное состояние, где сортировать и объединять будем? етс етс етс

Получается:

Либо RDBMS - все кусочками, четко, строго, компактно ... но затраты в момент получения на построение ответа. И, обычно, ограничено одним большим компьютером.

Либо NoSQL - затраты на хранение объемов, нет четкости, опечатки, сложный поиск по содержимому ... но в момент получения ~= скорости чтения с диска или из памяти.

Пример из документации Oracle

11.4.2.1 Overview of EXPLAIN PLAN

EXPLAIN PLAN FOR
SELECT 
    e.employee_id, 
    j.job_title, 
    e.salary, 
    d.department_name
FROM employees e
    JOIN jobs j on j.job_id = e.job_id
    JOIN departments d on d.department_id = e.department_id
WHERE  
    e.employee_id < 103;

Дай мне всех работников с работник-id < 103.

Верни мне, пожайлуста, их id, должность, зарплату и отдел

Данные хранятся в типах работник, работа, отдел

Связи работник - id-работы - работа, работник - id-отдела - отдел.

Мы декларировали что надо сделать, а не как надо сделать.

"ожидаемый" plan

-----------------------------------------------------------------------------------
| Id  | Operation                     |  Name        | Rows  | Bytes | Cost (%CPU)|
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |              |     3 |   189 |    10  (10)|
|   1 |  NESTED LOOPS                 |              |     3 |   189 |    10  (10)|
|   2 |   NESTED LOOPS                |              |     3 |   141 |     7  (15)|
|*  3 |    TABLE ACCESS FULL          | EMPLOYEES    |     3 |    60 |     4  (25)|
|   4 |    TABLE ACCESS BY INDEX ROWID| JOBS         |    19 |   513 |     2  (50)|
|*  5 |     INDEX UNIQUE SCAN         | JOB_ID_PK    |     1 |       |            |
|   6 |   TABLE ACCESS BY INDEX ROWID | DEPARTMENTS  |    27 |   432 |     2  (50)|
|*  7 |    INDEX UNIQUE SCAN          | DEPT_ID_PK   |     1 |       |            |
-----------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - filter("E"."EMPLOYEE_ID"<103)
   5 - access("E"."JOB_ID"="J"."JOB_ID")
   7 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID"

Wild guess: работников вернется N штук, тогда мы для каждого из них пойдем за работой по индексу (N запросов в индекс O(log(M)) + позиционирований O(1)) и для каждого пойдем в отделы так же по индексу (N раз * (log(D) + O(1))).

Где-то по пути еще будут работать кеши, так что оно чуть "сгладится" в реальной жизни.

реально исполненный план

--------------------------------------------------------------------------------------
 Id| Operation                       | Name          |Rows|Bytes|Cost(%CPU)| Time   |
--------------------------------------------------------------------------------------
  0| SELECT STATEMENT                |               |  3 | 189 |    8 (13)|00:00:01|
  1|  NESTED LOOPS                   |               |    |     |           |        |
  2|   NESTED LOOPS                  |               |  3 | 189 |    8 (13)|00:00:01|
  3|    MERGE JOIN                   |               |  3 | 141 |    5 (20)|00:00:01|
  4|     TABLE ACCESS BY INDEX ROWID | JOBS          | 19 | 513 |    2  (0)|00:00:01|
  5|      INDEX FULL SCAN            | JOB_ID_PK     | 19 |     |    1  (0)|00:00:01|
* 6|     SORT JOIN                   |               |  3 |  60 |    3 (34)|00:00:01|
  7|      TABLE ACCESS BY INDEX ROWID| EMPLOYEES     |  3 |  60 |    2  (0)|00:00:01|
* 8|       INDEX RANGE SCAN          | EMP_EMP_ID_PK |  3 |     |    1  (0)|00:00:01|
* 9|    INDEX UNIQUE SCAN            | DEPT_ID_PK    |  1 |     |    0  (0)|00:00:01|
 10|   TABLE ACCESS BY INDEX ROWID   | DEPARTMENTS   |  1 |  16 |    1  (0)|00:00:01|
--------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   6 - access("E"."JOB_ID"="J"."JOB_ID")
       filter("E"."JOB_ID"="J"."JOB_ID")
   8 - access("E"."EMPLOYEE_ID"<103)
   9 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")

Литература

Oracle: The Query Optimizer

MySQL Reference: Chapter 7 Optimization

MongoDB: Manual: Optimization

by Guy Harrison - эксперт в вопросах performance tuning баз данных:
* 10 things you should know about NoSQL databases
* книги по оптимизации
* blog

google:explain plan ИМЯ_БАЗЫ - ищите!

wikipedia:Query_Plan / wikipedia:План_выполнения_запроса

Правильный алгоритм любого ремонта

Итого:

Вопросы?