December 18, 2008

plpgsql и временные таблицы

Перевод plpgsql and temp. tables с Pavel Stehule's blog

Я провёл тестирование скорости трёх возможных стилей работы с временными таблицами в хранимых процедурах. Обычно я предпочитаю использовать проверку существования таблицы с помощью перехвата сообщения об ошибке. Я не ожидал этого, но перехват ошибок показал себя лучше всех.
create or replace function test1() 
returns void as $$
begin
drop table if exists omega;
create temp table omega(a integer);
insert into omega values(10);
if exists(select * from omega) then end if;
end;
$$ language plpgsql;

create or replace function test2()
returns void as $$
begin
if exists(select * from pg_class where relname='omega' and pg_table_is_visible(oid)) then
delete from omega;
else
create temp table omega(a integer);
end if;
insert into omega values(10);
if exists(select * from omega) then end if;
end;
$$ language plpgsql;

create or replace function test3()
returns void as $$
begin
begin
delete from omega;
exception
when others then
create temp table omega(a integer);
end;
insert into omega values(10);
if exists(select * from omega) then end if;
end;
$$ language plpgsql;
тест с помощью pgbench
Test1:
transaction type: Custom query
scaling factor: 1
query mode: simple
number of clients: 10
number of transactions per client: 1000
number of transactions actually processed: 10000/10000
tps = 339.780441 (including connections establishing)
tps = 340.172513 (excluding connections establishing)

Test2:
scaling factor: 1
query mode: simple
number of clients: 10
number of transactions per client: 1000
number of transactions actually processed: 10000/10000
tps = 1891.021562 (including connections establishing)
tps = 1907.533096 (excluding connections establishing)

Test3:
transaction type: Custom query
scaling factor: 1
query mode: simple
number of clients: 10
number of transactions per client: 1000
number of transactions actually processed: 10000/10000
tps = 2664.756569 (including connections establishing)
tps = 2698.289177 (excluding connections establishing

December 16, 2008

В ожидании 8.4 - pl/* srf функции в выборках

Перевод Waiting for 8.4 - pl/* srf functions in selects с select * from depesz;

28 октября Tom Lane применил свой патч изменяющий внутреннее устройство функций, что дало несколько интересных возможностей.

Комментарий к патчу:
Расширяет ExecMakeFunctionResult() поддержкой set-returning
функций, возвращающих значения с использованием tuplestore вместо механизма
значение-за-вызов. Проведён рефакторинг некоторых вещей, устраняющий дублирование
кода с помощью nodeFunctionscan.c. Это не обсуждаемая часть моего патча для перевода
SQL функций на возврат tuplestore. На данный момент SQL функции всё ещё ведут себя
по старому. Однако, теперь возможно использовать PL SRF функции в целевом списке
полей.
Что это даёт. Как вы возможно знаете, нельзя сделать join таблицы с функцией. Если функция возвращает >1 строки (или >1 колонки), не возможно её вызвать, передав ей в качестве аргумента поле из таблицы, используемой в запросе.

Я думаю описание немного сложновато, так что сразу перейдём к примеру.

Есть функция, которая, принимая 2 целых значения, возвращает все значения между ними, плюс некоторые текстовые поля:
CREATE OR REPLACE FUNCTION test(
IN from_i INT4,
IN to_i INT4,
OUT numerical INT4,
OUT textual TEXT
) RETURNS setof record as $$
DECLARE
i INT4;
BEGIN
for i in from_i .. to_i LOOP
numerical := i;
textual := 'i = ' || i;
RETURN next;
END loop;
RETURN;
END;
$$ language plpgsql;
Эта простая функция делает следующее:
# select * from test(2,5);
numerical | textual
-----------+---------
2 | i = 2
3 | i = 3
4 | i = 4
5 | i = 5
(4 rows)
Теперь, допустим, мы хотим (по какой-то причине) получить такие строки для каждой пары следующего выражения:
# select 1 as from_i, i as to_i from generate_series(1,3) i;
from_i | to_i
--------+------
1 | 1
1 | 2
1 | 3
(3 rows)
По большому счёту это затруднительно. Я не могу сделать join generate_series и моей функции. Если бы она была на SQL, то можно было бы сделать так
select i, test(...) from
но она на pl/pgsql. Конечно, я бы мог написать для неё SQL обёртку, но это совсем не здорово. К счастью, с новым патчем, pl/pgsql (и другие pl/*) функции смогут вызываться так же как и просто SQL функции:
# select i, test(1, i) from generate_series(1,3) i;
i | test
---+-------------
1 | (1,"i = 1")
2 | (1,"i = 1")
2 | (2,"i = 2")
3 | (1,"i = 1")
3 | (2,"i = 2")
3 | (3,"i = 3")
(6 rows)
Но что, если надо получить колонки отдельно?

Это можно сделать так:
select i, (test(1, i)).numerical, (test(1,i)).textual from generate_series(1,3) i;
Но в этом случае test() будет вызываться дважды для каждой строки, что будет проблематично, если там будет что-нибудь посложнее, чем просто цикл.

К счастью это можно также сделать простым подзапросом:
# select i, (test).numerical, (test).textual
from (select i, test(1, i) from generate_series(1,3) i) x;
i | numerical | textual
---+-----------+---------
1 | 1 | i = 1
2 | 1 | i = 1
2 | 2 | i = 2
3 | 1 | i = 1
3 | 2 | i = 2
3 | 3 | i = 3
(6 rows)
Превосходно. Ещё одна проблема позади.

*ДОПОЛНЕНИЕ*

(после напоминания от David Fetter)

CTE (основные табличные выражения) настолько новы, что я просто о них забыл, вот они:
with source as (
select i, test(1, i) from generate_series(1,3) i
)
select i, (test).numerical, (test).textual from source;
explain analyze

для CTE:
                                                         QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------
CTE Scan on source (cost=262.50..282.50 rows=1000 width=36) (actual time=2.331..2.534 rows=6 loops=1)
InitPlan
-> Function Scan on generate_series i (cost=0.00..262.50 rows=1000 width=4) (actual time=2.315..2.488 rows=6 loops=1)
Total runtime: 2.646 ms
(4 rows)
и для подзапроса:
                                                        QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Subquery Scan x (cost=0.00..272.50 rows=1000 width=36) (actual time=0.236..0.423 rows=6 loops=1)
-> Function Scan on generate_series i (cost=0.00..262.50 rows=1000 width=4) (actual time=0.229..0.395 rows=6 loops=1)
Total runtime: 0.477 ms
(3 rows)
От автора перевода:

Немного не понятно, почему в первом случае Function Scan занял больше времени. Возможно это было связано с разной нагрузкой на сервер во время обоих тестов. И я думаю, в данном случае смотреть надо на estimation, а не на actual time. Буду рад почитать ваши мысли по этому поводу в комментариях.

Кстати, я это не тестировал, но мне кажется, что достичь желаемого результата и без двойного вызова можно так
select i, (test(1, i)).* from generate_series(1,3) i;
Или я ошибаюсь?

*ДОПОЛНЕНИЕ*

Да, я оказался прав
# select i, (test(1, i)).* from generate_series(1,3) i;
i | numerical | textual
—+———–+———
1 | 1 | i = 1
2 | 1 | i = 1
2 | 2 | i = 2
3 | 1 | i = 1
3 | 2 | i = 2
3 | 3 | i = 3
(6 rows)

November 9, 2008

В ожидании 8.4 - RETURNING в подзапросах

Перевод Waiting for 8.4 - sql-wrappable RETURNING с select * from depesz;

В PostgreSQL 8.2 было добавлено выражение RETURNING для INSERT/UPDATE/DELETE запросов. К сожалению, оно не могло быть использовано как источник строк для всего в SQL.
insert into table_backup delete from table where ... returning *;
В прочем, это и сейчас не возможно, но был сделан один шаг в правильном направлении, благодаря патчу от Tom Lane 31го Октября:
Позволяет SQL-функциям возвращать вывод INSERT/UPDATE/DELETE RETURNING выражений, а не только SELECT как прежде.

Дополнительный эффект этого патча таков, что когда возвращающая множество SQL функция используется в FROM, производительность увеличивается за счёт того, что вывод накапливается в tuplestore внутри функции, в отличие от менее эффективного значение-за-вызов механизма.
Как это работает? Всё просто. Начнём с тестовой таблицы:
# create table test (i int4);
CREATE TABLE
С тестовым контентом:
# insert into test select generate_series(1, 10);
INSERT 0 10
Теперь создадим свою SQL функцию удаляющую строки:
CREATE function delete_from_test_returning(INT4) RETURNS setof test as $$
DELETE FROM test WHERE i <= $1 returning *
$$ language sql;
Как видно, функция очень проста.

Теперь используем её для бэкапа удаленных строк:
# create table delete_backup as select * from delete_from_test_returning(3);
SELECT
И проверим содержание обеих таблиц:
# select * from test;
i
----
4
5
6
7
8
9
10

(7 rows)

# select * from delete_backup;
i
---
1
2
3

(3 rows)
Конечно, я мог бы сделать это раньше в pl/pgsql функции, которая бы пробегалась по всем возвращаемым строкам, но в данном случае это определённо будет быстрее.

В ожидании 8.4 - Общие табличные выражения (WITH-запросы)

Перевод Waiting for 8.4 - Common Table Expressions (WITH queries) с select * from depesz;

Общее табличное выражение (от англ. Common Table Expressions, CTE) - временный именованный набор данных, полученный из простого запроса и определённый в области действия операций SELECT, INSERT, UPDATE, или DELETE.

4 сентября Tom Lane применил очередной замечательный патч. В этот раз он довольно весомый, т.ч. даже после принятия в нём остаются кое-какие недоделки. Понадобятся дополнительные патчи для того, чтобы реализовать полный функционал, но сам факт того, что он был принят означает его появление в 8.4.

Что же он делает?

Сначала посмотрим описание патча:
Реализует соответствующее SQL стандарту выражение WITH, включая WITH RECURSIVE.

Имеются некоторые не реализованные аспекты: рекурсивные запросы должны использовать
UNION ALL (использование UNION должно тоже позволяться) и у нас нет выражений SEARCH и CYCLE.

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

Так же имеется пара маленьких неопределённостей и ухищрений о которых я упоминал в
pgsql-hackers. Но давайте примем патч сейчас для того, чтобы привлечь других разработчиков.

Yoshiyuki Asaba, с огромной помощью Taysuo Ishii и Tom Lane.
Описание действительно выглядит интересно, ссылается на SQL стандарт, но что мы реально получаем?

Пример прямо из новой документации:
WITH regional_sales AS (
SELECT region, SUM(amount) AS total_sales
FROM orders
GROUP BY region
), top_regions AS (
SELECT region
FROM regional_sales
WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales)
)
SELECT region,
product,
SUM(quantity) AS product_units,
SUM(amount) AS product_sales
FROM orders
WHERE region IN (SELECT region FROM top_regions)
GROUP BY region, product;
Не сложно заметить, что запросы WITH не что иное как инлайновые view или лучше временные таблицы, но существующие только во время выполнения запроса.

Что это даёт?

Давайте проверим:
# create table orders (region int4, product int4, quantity int4, amount int4);
CREATE TABLE

# insert into orders (region, product, quantity, amount)
select random() * 1000, random() * 5000, 1 + random() * 20, 1 + random() * 1000
from generate_series(1,10000);

INSERT 0 10000
Во первых, попробуем запрос из документации (немного изменённый):
# explain analyze
>> WITH regional_sales AS (
>> SELECT region, SUM(amount) AS total_sales
>> FROM orders
>> GROUP BY region
>> ), top_regions AS (
>> SELECT region
>> FROM regional_sales
>> WHERE total_sales > (SELECT 2 * avg(total_sales) FROM regional_sales)
>> )
>> SELECT region,
>> product,
>> SUM(quantity) AS product_units,
>> SUM(amount) AS product_sales
>> FROM orders
>> WHERE region IN (SELECT region FROM top_regions)
>> GROUP BY region, product;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------
HashAggregate (cost=509.55..539.52 rows=1998 width=16) (actual time=71.882..72.254 rows=221 loops=1)
InitPlan
-> HashAggregate (cost=205.00..217.51 rows=1001 width=8) (actual time=31.532..33.234 rows=1001 loops=1)
-> Seq Scan on orders (cost=0.00..155.00 rows=10000 width=8) (actual time=0.005..13.888 rows=10000 loops=1)
-> CTE Scan on regional_sales (cost=22.54..47.56 rows=334 width=4) (actual time=39.250..39.719 rows=12 loops=1)
Filter: ((total_sales)::numeric > $1)
InitPlan
-> Aggregate (cost=22.52..22.54 rows=1 width=8) (actual time=7.666..7.667 rows=1 loops=1)
-> CTE Scan on regional_sales (cost=0.00..20.02 rows=1001 width=8) (actual time=0.002..4.786 rows=1001 loops=1)
-> Hash Join (cost=12.02..224.50 rows=1998 width=16) (actual time=40.069..71.422 rows=221 loops=1)
Hash Cond: (public.orders.region = top_regions.region)
-> Seq Scan on orders (cost=0.00..155.00 rows=10000 width=16) (actual time=0.011..16.861 rows=10000 loops=1)
-> Hash (cost=9.52..9.52 rows=200 width=4) (actual time=39.825..39.825 rows=12 loops=1)
-> HashAggregate (cost=7.51..9.52 rows=200 width=4) (actual time=39.785..39.805 rows=12 loops=1)
-> CTE Scan on top_regions (cost=0.00..6.68 rows=334 width=4) (actual time=39.254..39.762 rows=12 loops=1)
Total runtime: 72.674 ms

(16 rows)
Как вы можете видеть я изменил определение "top_regions" - вместо 10% продаж я определил лучшие регионы как более чем в два раза превысившие средние продажи.

Теперь попробуем переписать этот запрос без использования WITH:
# explain analyze
>> SELECT region,
>> product,
>> SUM(quantity) AS product_units,
>> SUM(amount) AS product_sales
>> FROM orders
>> WHERE region IN (
>> SELECT region
>> FROM orders
>> group by region
>> having sum(amount) > 2 * (
>> select avg(sum) from (
>> Select sum(amount) from orders group by region
>> ) as x
>> )
>> )
>> GROUP BY region, product;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------
HashAggregate (cost=710.04..740.01 rows=1998 width=16) (actual time=130.271..130.644 rows=221 loops=1)
-> Hash Join (cost=477.58..690.06 rows=1998 width=16) (actual time=85.452..129.649 rows=221 loops=1)
Hash Cond: (public.orders.region = public.orders.region)
-> Seq Scan on orders (cost=0.00..155.00 rows=10000 width=16) (actual time=0.011..16.427 rows=10000 loops=1)
-> Hash (cost=465.07..465.07 rows=1001 width=4) (actual time=85.157..85.157 rows=12 loops=1)
-> HashAggregate (cost=435.04..455.06 rows=1001 width=8) (actual time=83.615..85.127 rows=12 loops=1)
Filter: ((sum(public.orders.amount))::numeric > (2::numeric * $0))
InitPlan
-> Aggregate (cost=230.03..230.04 rows=1 width=8) (actual time=47.372..47.374 rows=1 loops=1)
-> HashAggregate (cost=205.00..217.51 rows=1001 width=8) (actual time=41.329..43.404 rows=1001 loops=1)
-> Seq Scan on orders (cost=0.00..155.00 rows=10000 width=8) (actual time=0.007..20.318 rows=10000 loops=1)
-> Seq Scan on orders (cost=0.00..155.00 rows=10000 width=8) (actual time=0.005..15.737 rows=10000 loops=1)
Total runtime: 131.050 ms
(13 rows)
Как видно - это медленней. Причина очень проста. "Старый" запрос вынужден сканировать таблицу 3 раза.

Новый это делает только 2 раза.

WITH может быть использован для написания более читаемых запросов работающих с меньшим количеством данных и, соответственно, более быстрых.

Но это не всё. Также существует специальная форма WITH запросов, которые могут дать эффект, не достижимый прежде. Это WITH RECURSIVE.

Представим простейшую древовидную структуру:
create table tree (id serial primary key, parent_id int4 references tree (id));
insert into tree (parent_id) values (NULL);
insert into tree (parent_id)
select case when random() < 0.95 then floor(1 + random() * currval('tree_id_seq')) else NULL end
from generate_series(1,1000) i;
Тут создаётся "лес" (не просто "дерево", т.к. имеют место несколько корневых элементов), который имеет (с моими случайными данными):

- 1001 элемент
- 56 корневых узлов
- 28 корневых узлов содержащих дочерние элементы
- самый длинный путь содержит 16 узлов: 1 -> 2 -> 3 -> 7 -> 12 -> 15 -> 39 -> 52 -> 61 -> 107 -> 123 -> 194 -> 466 -> 493 -> 810 -> 890

Традиционно, если понадобится вывести всех родителей узла 890, потребуется написать цикл делающий запросы до тех пор, пока не будет получена строка, где "parent_id IS NULL".

Но сейчас мы можем сделать следующее:
WITH RECURSIVE struct AS (
SELECT t.* FROM tree t WHERE id = 890
UNION ALL
SELECT t.* FROM tree t, struct s WHERE t.id = s.parent_id
)
SELECT * FROM struct;
Что работает действительно здорово:
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------
CTE Scan on struct (cost=402.18..404.20 rows=101 width=8) (actual time=0.037..69.311 rows=16 loops=1)
InitPlan
-> Recursive Union (cost=0.00..402.18 rows=101 width=8) (actual time=0.030..69.222 rows=16 loops=1)
-> Index Scan using tree_pkey on tree t (cost=0.00..8.27 rows=1 width=8) (actual time=0.024..0.029 rows=1 loops=1)
Index Cond: (id = 890)
-> Hash Join (cost=0.33..39.19 rows=10 width=8) (actual time=2.165..4.313 rows=1 loops=16)
Hash Cond: (t.id = s.parent_id)
-> Seq Scan on tree t (cost=0.00..35.01 rows=1001 width=8) (actual time=0.005..2.434 rows=1001 loops=15)
-> Hash (cost=0.20..0.20 rows=10 width=4) (actual time=0.012..0.012 rows=1 loops=16)
-> WorkTable Scan on struct s (cost=0.00..0.20 rows=10 width=4) (actual time=0.002..0.005 rows=1 loops=16)
Total runtime: 69.445 ms
(11 rows)
Вас может смутить наличие "Seq Scan on tree…loops=15", но не стоит беспокоиться. Это так из-за очень малого количества строк в таблице.

После добавления дополнительных 50000 строк получаем:
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
CTE Scan on struct (cost=840.76..842.78 rows=101 width=8) (actual time=0.039..0.672 rows=16 loops=1)
InitPlan
-> Recursive Union (cost=0.00..840.76 rows=101 width=8) (actual time=0.032..0.591 rows=16 loops=1)
-> Index Scan using tree_pkey on tree t (cost=0.00..8.27 rows=1 width=8) (actual time=0.025..0.029 rows=1 loops=1)
Index Cond: (id = 890)
-> Nested Loop (cost=0.00..83.05 rows=10 width=8) (actual time=0.018..0.027 rows=1 loops=16)
-> WorkTable Scan on struct s (cost=0.00..0.20 rows=10 width=4) (actual time=0.002..0.004 rows=1 loops=16)
-> Index Scan using tree_pkey on tree t (cost=0.00..8.27 rows=1 width=8) (actual time=0.008..0.011 rows=1 loops=16)
Index Cond: (t.id = s.parent_id)
Total runtime: 0.799 ms
(10 rows)
Что выглядит превосходно.

Как я упоминал ранее, остаются вещи нуждающиеся в доработке. Но даже сейчас WITH запросы выглядят великолепно.

November 8, 2008

Фикс облака тэгов

Решил избавиться от количества записей в облаке тэгов. По моему так оно получше смотрится. Как считаете?

November 5, 2008

Почему мой индекс не используется

Перевод Why is my index not being used с Postgres OnLine Journal

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

В этой статье мы кратко опишем основные причины и попробуем упорядочить их по статистической значимости.

Если кто не в курсе, то узнать о том, что ваш индекс используется или не используется можно с помощью инструкций EXPLAIN, EXPLAIN ANALYZE, или, например, продвинутого инструмента графического объяснения планов в PgAdmin.

Ок, допустим запрос не использует индекс. Почему и что можно с этим сделать?

Проблема: Устаревшая статистика.
Сейчас это менее вероятно в связи с тем, что по умолчанию auto-vacuum включён. Но если в таблице только что было обновлено/добавлено/удалено много записей, или был добавлен новый индекс, то есть вероятность, что во время выполнения запроса новая статистика ещё не соберётся.

Решение: vacuum analyze verbose ;
Я добавил verbose для того, что бы видеть что делает сборка мусора/статистики, и, если вы настолько же нетерпеливы как и я и ваша таблица довольно большая, то здорово видеть что что-то в самом деле происходит. Так же можно выполнить vacuum analyze verbose без указания таблицы, если необходимо провести профилактику всей БД.

Проблема: Планировщик решил, что последовательное сканирование быстрее чем индексное.
Это может случиться если а) ваша таблица относительно мала, или поле, по которому вы индексируете, содержит много дубликатов.

Решение: В случае использования, например boolean, когда 50% данных содержат одно значение, а вторая половина другое, индексирование будет не очень полезным. Однако, это неплохая причина для использования частичных индексов, например для индексирования только активных данных.

Проблема: Вы создали индекс, который не совместим с тем, как вы фильтруете данные. Есть ряд таких случаев.

1. LIKE '%me' никогда не будет использовать индексы, но можно использовать LIKE 'me%'.

2. Ловушка с upper/lower - если определить индекс как:
CREATE INDEX idx_faults_name ON faults USING btree(fault_name);
и выполнить такой запрос
SELECT * FROM faults where UPPER(fault_name) LIKE 'CAR%'
то индекс не будет использоваться. Тут необходимо создать индекс следующим образом:
CREATE INDEX idx_faults_name ON faults USING btree(upper(fault_name));

3. Этого не избежал даже я, всегда полезно почитать о проблемах других людей в новостных группах, т.к. вы узнаёте о вещах, о которых даже не подозревали. Если ваш сервер инициализирован с C-локалью, выше сказанное опять же не будет работать. Подобный вопрос недавно было в группе pgsql-novice, где Tom Lane дал подробный ответ, и это, по моему, затрагивает многих. Ситуацию возможно исправит:
CREATE INDEX idx_faults_uname_varchar_pattern ON faults USING btree(upper(fault_name) varchar_pattern_ops);
Однако, даже в этом случае может потребоваться учитывать описанное ниже:

Не совсем понятно является ли это проблемой кодировки БД, самих данных или разницы версий 8.3 b 8.2. Похоже, что в 8.3 в конкретных случаях с данными в кодировке UTF-8 требуется точное сравнение, однако в подобных ситуациях с 8.2 в SQL-ASCII достаточно varchar_pattern_ops как для точного сравнения так и для LIKE.
CREATE INDEX idx_faults_uname ON faults USING btree(upper(fault_name)); 

SELECT fault_name from faults
WHERE upper(fault_name) IN('CASCADIA ABDUCTION', 'CABIN FEVER');

4. Ошибки новичков, когда делается что-нибудь типа создания индекса по дате и сравнение текста с этой датой приведённой к тексту.

Проблема: Не все индексы могу быть использованы.
Не смотря на то, что с версии 8.1+ поддерживаются Bitmap Index Scan, позволяющие множеству индексов быть использованными в запросе путём создания в памяти bitmap индексов, если индексов много, не ждите, что будут использоваться все ожидаемые. Иногда сканирование таблицы бывает эффективнее.

Проблема: Планировщик не идеален.
Решение: Рыдать и молиться о более светлых временах. На самом деле я был очень потрясён возможностями планировщика Postgresql в сравнении с другими СУБД. Некоторые говорят, если бы только тут были "хинты", я бы сделал это работающим быстрее. Я же думаю, что хинты это плохая идея и лучшим решением было бы сделать планировщик лучше. Проблема хинтов в том, что они отодвигают на второй план одну хорошую вещь, идею того, что СУБД знает состояние данных лучше чем человек и постоянно обновляет это знание. Хинты могут быстро стать не актуальными, в то время когда хороший планировщик будет постоянно менять стратегию по мере изменений в БД, и это то что делает программирование баз данных уникальным.

Leo Hsu and Regina Obe

October 22, 2008

Warning! Do not migrate to 8.3.4

Hi all,

Serious bug was found in 8.3.4 yesterday. If your DB deals with finance, privat security and so on - I advise you not to migrate to 8.3.4 postgres version. 8.3.3 does not contain this bug.

Additional info you can find here http://archives.postgresql.org/pgsql-general/2008-10/msg00845.php

Важно! Не спешите переходить на 8.3.4

Всем привет,

Вчера в версии postgresql 8.3.4 был найден серьёзный баг. Если ваша БД ответственна за финансовые операции, безопасность и т.п. - не советую вам обновляться до 8.3.4. Версия 8.3.3 этой ошибки не содержит.

Подробности тут http://archives.postgresql.org/pgsql-general/2008-10/msg00845.php

October 16, 2008

В ожидании 8.4 - новый FSM (Free Space Map)

Перевод Waiting for 8.4 - new FSM (Free Space Map) с select * from depesz;

30го сентября Heikki Linnakangas применил свой патч, который вносит изменения в FSM:
Переписан FSM. Вместо того, чтобы полагаться на фиксированный (указанный) размер 
сегмента разделяемой памяти, информация о свободном месте теперь хранится в
отдельном FSM отношении для каждого отношения БД (за исключением hash индексов; они
не используют FSM).

Это устраняет необходимость в max_fsm_relations и max_fsm_pages GUC параметрах;
удалены все их вхождения в бэкэнд, initdb и документацию.

Переписан contrib/pg_freespacemap в соответствии с новой FSM реализацией. Так же
представлен новый вариант функции get_raw_page(regclass, int4, int4) в
contrib/pageinspect, который позволяет увидеть страницы любого отношения, и новая
функция fsm_page_contents() для проверки новых FSM страниц.
Что это значит для DBA?

Для начала, отпадает необходимость в настройке 2х параметров в postgresql.conf: max_fsm_pages и max_fsm_relations.

Эти параметры (когда установлены не корректно) могут сделать vacuum менее эффективным (что происходит очень часто). Т.ч., в основе, это хорошо, что их больше не будет.

Что ещё? Чтобы сделать это Haikki пришлось реализовать так называемые "дополнительные отношения" ("Relation forks"). И это важно, потому что (насколько я понимаю, если я понял не правильно, пожалуйста, поправьте меня) они могут (и наверняка будут) использоваться для хранения "карт видимости" ("visibility maps"), которые сделают vacuum более быстрым (и возможно повлияют на индексные сканирования, но это только моя догадка).

Что же это за "дополнительные отношения"? Всё просто. Как известно, таблицы хранятся в файлах следующим образом:
$PGDATA/base/<database-oid>/<table-filenode>
Иногда они имеют суфиксы .1, .2 и т.д. - в случае если размер таблицы (или индекса) превышеает гигабайт.

Дополнительные отношения добавляют второй набор файлов, именуемых:
<table-filenode>_1
Для FSM код "1" (но ходят разговоры о текстовых кодах).

Пример:
# create table x (id int4);
CREATE TABLE

# select oid from pg_database where datname = 'depesz';
oid
-------
16385
(1 row)

# select relfilenode from pg_class where relname = 'x' and relkind = 'r';
relfilenode
-------------
16387
(1 row)

=> ls -l $PGDATA/base/16385/16387*
-rw------- 1 pgdba pgdba 0 2008-10-04 12:50 /home/pgdba/data/base/16385/16387
-rw------- 1 pgdba pgdba 0 2008-10-04 12:50 /home/pgdba/data/base/16385/16387_1
Конечно же, теперь FSM хранит полную информацию и не ограничен в размере. Интересно, как много места он занимает. Давайте проверим:
# insert into x (id) select * from generate_series(1,100000);
INSERT 0 100000

# \! ls -l $PGDATA/base/16385/16387*
-rw------- 1 pgdba pgdba 3219456 2008-10-04 12:53 /home/pgdba/data/base/16385/16387
-rw------- 1 pgdba pgdba 24576 2008-10-04 12:53 /home/pgdba/data/base/16385/16387_1
# insert into x (id) select * from generate_series(1,100000);
INSERT 0 100000

# \! ls -l $PGDATA/base/16385/16387*
-rw------- 1 pgdba pgdba 6430720 2008-10-04 12:53 /home/pgdba/data/base/16385/16387
-rw------- 1 pgdba pgdba 24576 2008-10-04 12:53 /home/pgdba/data/base/16385/16387_1
# insert into x (id) select * from generate_series(1,100000);
INSERT 0 100000

# \! ls -l $PGDATA/base/16385/16387*
-rw------- 1 pgdba pgdba 9641984 2008-10-04 12:53 /home/pgdba/data/base/16385/16387
-rw------- 1 pgdba pgdba 24576 2008-10-04 12:53 /home/pgdba/data/base/16385/16387_1
Ок, отсюда видно, что он не увеличивается в размере, когда я добавляю записи в таблицу.

Но, может это из-за того, что таблица очень маленькая, всего 1177 страниц. Проверим на чём-нибудь большем:
# drop table x;
DROP TABLE

# create table x (id int4, dummy_text text);
CREATE TABLE

# alter table x alter column dummy_text set storage plain;
ALTER TABLE

# select relfilenode from pg_class where relname = 'x' and relkind = 'r';
relfilenode
-------------
16408
(1 row)
На заметку: я сделал alter column set storage plain для хранения всех данных из dummy_text в главной таблице, без компресси - эффективное отключение TOAST.
# insert into x select i, repeat('depesz', 500) from generate_series(1,100000) as i;
INSERT 0 100000

# \! ls -l $PGDATA/base/16385/16408*
-rw------- 1 pgdba pgdba 409600000 2008-10-04 13:05 /home/pgdba/data/base/16385/16408
-rw------- 1 pgdba pgdba 122880 2008-10-04 13:05 /home/pgdba/data/base/16385/16408_1
И что случится, если я сделаю update 50% записей?
# update x set dummy_text = repeat('_test_', 500) where id <= 50000;
UPDATE 50000

# \! ls -l $PGDATA/base/16385/16408*
-rw------- 1 pgdba pgdba 614400000 2008-10-04 13:09 /home/pgdba/data/base/16385/16408
-rw------- 1 pgdba pgdba 172032 2008-10-04 13:08 /home/pgdba/data/base/16385/16408_1
Это говорит, что излишек данных на диске составляет около 0.03%, что является незначительным.

Выгоды? У нас теперь одним поводом для беспокойства меньше (слишком маленькие значения параметров FSM) и основание для будущего кода, который сделает vacuum быстрее. Намного быстрее.

October 14, 2008

В ожидании 8.4 - упорядоченная загрузка данных в дамп

Перевод Waiting for 8.4 - ordered data loading in pg_dump с select * from depesz;

Отличный (и, надо сказать, давно ожидаемый) патч от Tom Lane:
Теперь pg_dump --data-only пытается упорядочить дампы таблиц таким образом,
что таблицы, на которые ссылаются вторичные ключи, выгружаются раньше таблиц,
содержащих эти ключи. Это помогает обходить сбои, возникающие при загрузке
данных,которые ссылаются на ещё не загруженные данные. Когда такое упорядочивание
не возможно, в случае циклических зависимостей или ссылок на самих себя, выводит
NOTICE для предупреждения об этом пользователя.
Что это означает в действительности?

Начнём с простого допущения - этот патч затрагивает выгрузку только данных (--data-only). Так что, если вы это не используете, то вам оно ничем не поможет. Простите.

Но, если используете, и у вас "пропатченая" версия postgres, вот что произойдёт:

Во первых, создадим кое-какие таблицы:
# create table b (id serial primary key);
NOTICE: CREATE TABLE will create implicit sequence "b_id_seq" for serial column "b.id"
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "b_pkey" for table "b"
CREATE TABLE

# create table a (id serial primary key, b_id int4 references b (id));
NOTICE: CREATE TABLE will create implicit sequence "a_id_seq" for serial column "a.id"
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "a_pkey" for table "a"
CREATE TABLE
И добавим данных:
# insert into b (id) values (DEFAULT), (DEFAULT), (DEFAULT);
INSERT 0 3
# insert into a (b_id) values (1), (2), (3);
INSERT 0 3
Всё просто, вот данные:
# select * from b;
id
----
1
2
3
(3 rows)

# select * from a;
id | b_id
----+------
1 | 1
2 | 2
3 | 3
(3 rows)
Вот что произойдёт если я сделаю дамп без патча:
=> pg_dump --data-only
...
COPY a (id, b_id) FROM stdin;
1 1
2 2
3 3
\.
...
COPY b (id) FROM stdin;
1
2
3
\.
...
Печально - данные будут загружены в таблицу a (с FK на b) перед тем, как загрузится b.

Конечно же данные могут быть загружены иначе, через выключение триггеров вторичных ключей (ALTER TABLE … DISABLE TRIGGER), но это сложновато, и определённо не "круто".

С новым патчем дамп выглядит иначе:
...
COPY b (id) FROM stdin;
1
2
3
\.
...
COPY a (id, b_id) FROM stdin;
1 1
2 2
3 3
\.
...
Порядок теперь правильный. Конечно это не сработает в случае циклической зависимости:
# create table a (id serial primary key);
# create table b (id serial primary key, a_id int4 references a(id));
# alter table a add column b_id int4 references b (id);
# insert into a (id) values (DEFAULT);
# insert into b (id, a_id) values (1, 1);
# insert into a (b_id) values (1);
Будет выведено предупреждение:
pg_dump: NOTICE: there are circular foreign-key constraints among these table(s):
pg_dump: a
pg_dump: b
pg_dump: You may not be able to restore the dump without using --disable-triggers or temporarily dropping the constraints.
pg_dump: Consider using a full dump instead of a --data-only dump to avoid this problem.
Хорошее дополнение. И респект Tom'у.

October 13, 2008

В ожидании 8.4 - lc_collation и lc_ctype уровня БД

Перевод Waiting for 8.4 - database-level lc_collation and lc_ctype с select * from depesz;

23 сентября Heikki Linnakangas применил патч, который написал Radek Strnad (на самом деле была применена его доработанная версия).

Что он делает? Патч позволяет добавлять (действительно!) разные collation order и character categories для разных БД.

До этого надо было устанавливать LC_COLLATE и LC_CTYPE при инициализации кластера (initdb), и в дальнейшем эти параметры невозможно было изменить. БД инициализированная с LATIN2 не будет корректно работать с данными в UTF-8.

Теперь всё меняется:
Делает LC_COLLATE и LC_CTYPE параметрами уровня БД. Порядок 
сортировки (collation) и класс символов (ctype) сейчас, подобно
кодировке, хранятся в новых колонках datcollate и datctype
таблицы database.

Это доработанная мной версия патча Radek Strnad'а.
Что мы получаем?

Например, предположим, что есть инстанс PostgreSQL инициализированный с локалью "C":
# \l
List of databases
Name | Owner | Encoding | Collation | Ctype | Access Privileges
-----------+--------+-----------+-----------+-------+----------------------------
depesz | depesz | SQL_ASCII | C | C |
postgres | pgdba | SQL_ASCII | C | C |
template0 | pgdba | SQL_ASCII | C | C | {=c/pgdba,pgdba=CTc/pgdba}
template1 | pgdba | SQL_ASCII | C | C | {=c/pgdba,pgdba=CTc/pgdba}
(4 rows)
Т.к. "C" ничего не знает, например, о польских символах, не возможно будет корректно сделать сортировку по польскому тексту. Это же касается работы upper():
# set client_encoding = 'UTF-8';
SET

# select c, upper(c) from (values ('a'), ('ć'), ('e'), ('ź'), ('x'), ('ł'), ('ś'), ('w')) as x (c) order by c;
c | upper
---+-------
a | A
e | E
w | W
x | X
ć | ć
ł | ł
ś | ś
ź | ź
(8 rows)
(Если вы не знакомы с польским алфавитом, просто поверьте мне. Сейчас я покажу как это должно выглядеть правильно.).

С версией postgres <= 8.3 мне бы потребовалось переинициализировать кластер (reinitdb), и, соответственно, сконвертировать все базы для новых языковых параметров, что немного проблематично.

К счастью, с версии 8.4 я смогу просто добавить новую базу с другими параметрами:
# CREATE DATABASE depesz_pl with encoding 'utf8' collate 'pl_PL.UTF-8' ctype 'pl_PL.UTF-8' template template0;
CREATE DATABASE
Единственная проблема в необходимости использования template0. Иначе я получу:
# CREATE DATABASE depesz_pl with encoding 'utf8' collate 'pl_PL.UTF-8' ctype 'pl_PL.UTF-8';
ERROR: new collation is incompatible with the collation of the template database (C)
HINT: Use the same collation as in the template database, or use template0 as template
(конечно же я мог бы сделать template1_pl, но не сейчас - как-нибудь в следующий раз :)

И так, теперь у нас есть новая БД, попробуем на ней наш тестовый запрос:
# \c depesz_pl
You are now connected to database "depesz_pl".

# show client_encoding ;
client_encoding
-----------------
UTF8
(1 row)

# select c, upper(c) from (values ('a'), ('ć'), ('e'), ('ź'), ('x'), ('ł'), ('ś'), ('w')) as x (c) order by c;
c | upper
---+-------
a | A
ć | Ć
e | E
ł | Ł
ś | Ś
w | W
x | X
ź | Ź
(8 rows)
ДА! Заработало!

Также, команда \l выдаст новую информацию:
# \l
List of databases
Name | Owner | Encoding | Collation | Ctype | Access Privileges
-----------+--------+-----------+-------------+-------------+----------------------------
depesz | depesz | SQL_ASCII | C | C |
depesz_pl | depesz | UTF8 | pl_PL.UTF-8 | pl_PL.UTF-8 |
postgres | pgdba | SQL_ASCII | C | C |
template0 | pgdba | SQL_ASCII | C | C | {=c/pgdba,pgdba=CTc/pgdba}
template1 | pgdba | SQL_ASCII | C | C | {=c/pgdba,pgdba=CTc/pgdba}
(5 rows)
Одна вещь, которую надо принять - нельзя изменить collation/ctype существующей базы. Причина тому очень проста: индексы зависят от collation (и могут зависеть от ctype). Т.о. изменение collation/ctype потребуют переиндексации всех данных. Если это технически возможно, вы сможете сделать это с помошью дампа базы, создания новой с желаемой локалью и загрузки этого дампа.

Конечно же, ещё далеко до полноценного функционирования PostgreSQL в мультиязычном окружении, но по крайней мере это шаг в правильном направлении. Долгожданный и важный шаг.

Комментарии

Steve, 29 сентября 2008 в 06:43
Вы упомянули "ещё далеко до полноценного функционирования PostgreSQL в мультиязычном окружении". Не могли бы вы рассказать в чём нехватка полноценного функционирования и где могут быть проблемы?

depesz, 29 сентября 2008 в 09:55
@Steve:
Для того чтобы сделать это полнофункциональным, необходима поддержка collation/ctype на более мелких объектах, чем база данных: таблицы или колонки.

Например, для сортировки по тексту на нескольких языках. Конечно, можно везде использовать utf8, но это приводит к дополнительным затратам на конвертацию текста при использовании различных кодировок.

October 8, 2008

postgresql error: more than one function named

Привет

В последнне время, анализируя свой Google Analytics, я заметил, что самым частым поисковым запросом от русскоязычной аудитории к Google, который приводит на мой блог является "postgresql error: more than one function named". Если вы пришли на эту страницу по такому запросу, оставьте, пож., описание ситуации в которой возникает данная ошибка и свой e-mail в комментариях. Я постараюсь найти ответ и обязательно напишу вам.

p.s. Напоминаю, что консультацию по postgresql вы можете получить практически в любое время воспользовавшись следующими ресурсами

IRC: #postgresql-ru@irc.freenode.net (KOI8-R)
Jabber: postgresmen@conference.jabber.org
Web: http://forum.postgresmen.ru

Всё это абсолютно свободно

С уважением,
Сергей Коноплёв

Space-time: cube and btree_gist use

Hello

I want to share my recent experience with geometric data processing and optimization in postgresql. The solution I'm going to describe has produced a good practical effect.

Let's assume we have a table for geo data logging geodata_log containing objects location column obj_point (point) and capture time obj_created (timestamp). Also there's a query that is used to obtain the latest object from specified square very often:
EXPLAIN ANALYZE
SELECT * FROM
geodata_log
WHERE
box(obj_point, obj_point) <@ box(point(50.857639595549, 30.337280273438), point(55.450349029297, 38.715576171875))
ORDER BY
obj_created
LIMIT 1;
Adding data into the table is rather intencive so we can ensure that at least one record has been added for the last 3 months. Accordingly we can supply the query with additional condition on capture time:
EXPLAIN ANALYZE
SELECT * FROM
geodata_log
WHERE
box(obj_point, obj_point) <@ box(point(50.857639595549, 30.337280273438), point(55.450349029297, 38.715576171875))
AND obj_created > '2008-07-04'
ORDER BY
obj_created
LIMIT 1;
It's clear that case without indexes doesn't worth our attention. So let's create an index on location and capture time:
CREATE INDEX i_geodata_log__created_point
ON geodata_log
USING gist
(obj_created, box(obj_point, obj_point));
And we've got an error:
ERROR: data type timestamp with time zone has no default operator class for access method "gist"
HINT: You must specify an operator class for the index or define a default operator class for the data type.
Similar message we will see if we try to create btree index. The thing is that int4, timestamp and other common types are supported by btree oposite to geometric types that are supported by gist.

Let's create two indexes, first on location:
CREATE INDEX i_geodata_log__point
ON geodata_log
USING gist
(box(obj_point, obj_point));
Query plan is:
Limit  (cost=256.03..256.04 rows=1 width=570) (actual time=17.081..17.082 rows=1 loops=1)
-> Sort (cost=256.03..256.04 rows=1 width=570) (actual time=17.079..17.079 rows=1 loops=1)
Sort Key: obj_created
Sort Method: top-N heapsort Memory: 17kB
-> Bitmap Heap Scan on geodata_log (cost=4.79..256.02 rows=1 width=570) (actual time=9.903..15.112 rows=829 loops=1)
Recheck Cond: (box(obj_point, obj_point) <@ '(55.450349029297,38.715576171875),(50.857639595549,30.337280273438)'::box)
Filter: (obj_created > '2008-07-04 00:00:00+04'::timestamp with time zone)
-> Bitmap Index Scan on i_geodata_log__point (cost=0.00..4.79 rows=67 width=0) (actual time=9.231..9.231 rows=2362 loops=1)
Index Cond: (box(obj_point, obj_point) <@ '(55.450349029297,38.715576171875),(50.857639595549,30.337280273438)'::box
Total runtime: 17.184 ms
Second one on capture time:
CREATE INDEX i_geodata_log__obj_created
ON geodata_log
USING btree
(obj_created);
Plan is:
Limit  (cost=40.44..40.45 rows=1 width=570) (actual time=15.288..15.288 rows=1 loops=1)
-> Sort (cost=40.44..40.45 rows=1 width=570) (actual time=15.285..15.285 rows=1 loops=1)
Sort Key: obj_created
Sort Method: top-N heapsort Memory: 17kB
-> Bitmap Heap Scan on geodata_log (cost=36.42..40.43 rows=1 width=570) (actual time=10.909..13.297 rows=829 loops=1)
Recheck Cond: ((box(obj_point, obj_point) <@ '(55.450349029297,38.715576171875),(50.857639595549,30.337280273438)'::box) AND (obj_created > '2008-07-04 00:00:00+04'::timestamp with time zone))
-> BitmapAnd (cost=36.42..36.42 rows=1 width=0) (actual time=10.751..10.751 rows=0 loops=1)
-> Bitmap Index Scan on i_geodata_log__point (cost=0.00..4.79 rows=67 width=0) (actual time=9.235..9.235 rows=2362 loops=1)
Index Cond: (box(obj_point, obj_point) <@ '(55.450349029297,38.715576171875),(50.857639595549,30.337280273438)'::box)
-> Bitmap Index Scan on i_geodata_log__obj_created (cost=0.00..31.38 rows=1483 width=0) (actual time=0.996..0.996 rows=3857 loops=1)
Index Cond: (obj_created > '2008-07-04 00:00:00+04'::timestamp with time zone)
Total runtime: 15.392 ms
Despite the index scans the situation wishes to be desired. Is there another way? It is.

Take a look at postgres contribs and pay attention to cube and btree_gist. First provides a cube type - multidimentional cube of float point numbers. Second implements gist for int2, int4, timestamp and other common types.

cube will help us if we represent our space like float triplets (latitude, longitude, seconds_since_epoch). Btree allows to create mixed indexes. Let's install both contribs and test it.

cube requires additional column, let's name it area_time:
ALTER TABLE geodata_log
ADD COLUMN area_time cube;
It has to be filled with 3D points (obj_point[0], obj_point[1], extract(epoch from obj_created)):
UPDATE geodata_log SET
area_time = cube(ARRAY[obj_point[0], obj_point[1], extract(epoch from obj_created)],
ARRAY[obj_point[0], obj_point[1], extract(epoch from obj_created)]);
Create an index on it:
CREATE INDEX i_geodata_log__area_time
ON geodata_log
USING gist
(area_time);
Change our query and look at the plan:
EXPLAIN ANALYZE
SELECT * FROM
geodata_log
WHERE
area_time <@ cube(ARRAY[50.857639595549, 30.337280273438, extract(epoch from '2008-07-04'::timestamp)],
ARRAY[55.450349029297, 38.715576171875, extract(epoch from now()::timestamp)])
ORDER BY
obj_created
LIMIT 1;

Limit (cost=265.36..265.36 rows=1 width=614) (actual time=5.312..5.313 rows=1 loops=1)
-> Sort (cost=265.36..265.52 rows=67 width=614) (actual time=5.310..5.310 rows=1 loops=1)
Sort Key: obj_created
Sort Method: top-N heapsort Memory: 17kB
-> Bitmap Heap Scan on geodata_log (cost=8.83..265.02 rows=67 width=614) (actual time=0.794..3.190 rows=829 loops=1)
Recheck Cond: (area_time <@ cube('{50.857639595549,30.337280273438,1215115200}'::double precision[], ARRAY[55.450349029297::double precision, 38.715576171875::double precision, date_part('epoch'::text, (now())::timestamp without time zone)]))
-> Bitmap Index Scan on i_geodata_log__area_time (cost=0.00..8.81 rows=67 width=0) (actual time=0.682..0.682 rows=829 loops=1)
Index Cond: (area_time <@ cube('{50.857639595549,30.337280273438,1215115200}'::double precision[], ARRAY[55.450349029297::double precision, 38.715576171875::double precision, date_part('epoch'::text, (now())::timestamp without time zone)]))
Total runtime: 5.420 ms
It's better, indeed. Now try btree_gist:
CREATE INDEX i_geodata_log__created_point
ON geodata_log
USING gist
(obj_created, box(obj_point, obj_point));

EXPLAIN ANALYZE
SELECT * FROM
geodata_log
WHERE
box(obj_point, obj_point) <@ box(point(50.857639595549, 30.337280273438), point(55.450349029297, 38.715576171875))
AND obj_created > '2008-07-04'
ORDER BY
obj_created
LIMIT 1;

Limit (cost=8.31..8.32 rows=1 width=570) (actual time=5.092..5.093 rows=1 loops=1)
-> Sort (cost=8.31..8.32 rows=1 width=570) (actual time=5.092..5.092 rows=1 loops=1)
Sort Key: obj_created
Sort Method: top-N heapsort Memory: 17kB
-> Index Scan using i_geodata_log__created_point on geodata_log (cost=0.00..8.30 rows=1 width=570) (actual time=0.076..3.408 rows=829 loops=1)
Index Cond: ((obj_created > '2008-07-04 00:00:00+04'::timestamp with time zone) AND (box(obj_point, obj_point) <@ '(55.450349029297,38.715576171875),(50.857639595549,30.337280273438)'::box))
Total runtime: 5.170 ms
Quite good.

Making few tests in different situations I noticed that performance is almost the same in both cases but as cube requires additional column and trigger to fill it up I choose btree_gist.

What would you choose depends on your situation.

p.s. In the nearest future I'm going to publish a post named "Divide and conquer: active and arcive data problem" It'll be interesting I promise :)

Regards,
Sergey Konoplev

October 6, 2008

Пространство и время: применение cube и btree_gist

Привет

Хочу поделиться своим недавним опытом оптимизации работы с геометрическими данными в postgresql. На практике это решение показало очень хороший результат.

Допустим есть таблица для журналирования геоданных geodata_log, содержащая местоположение объекта obj_point типа point и время фиксации obj_created типа timestamp. По ней очень часто выполняется запрос получения самого "свежего" объекта в заданной области:
EXPLAIN ANALYZE
SELECT * FROM
geodata_log
WHERE
box(obj_point, obj_point) <@ box(point(50.857639595549, 30.337280273438), point(55.450349029297, 38.715576171875))
ORDER BY
obj_created
LIMIT 1;
Таблица достаточно интенсивная, т.е. мы с уверенностью можем сказать, что за последние 3 месяца туда добавлялась хотя бы одна запись. Соответственно сразу имеет смысл ограничиться по времени фиксации:
EXPLAIN ANALYZE
SELECT * FROM
geodata_log
WHERE
box(obj_point, obj_point) <@ box(point(50.857639595549, 30.337280273438), point(55.450349029297, 38.715576171875))
AND obj_created > '2008-07-04'
ORDER BY
obj_created
LIMIT 1;
Ситуацию без индексов я не рассматриваю в силу очевидности появления плохого плана. И так, создаём индекс по времени фиксации и местоположению:
CREATE INDEX i_geodata_log__created_point
ON geodata_log
USING gist
(obj_created, box(obj_point, obj_point));
И видим ошибку:
ERROR: data type timestamp with time zone has no default operator class for access method "gist"
HINT: You must specify an operator class for the index or define a default operator class for the data type.
Подобное сообщением мы увидим если попробуем создать аналогичный btree индекс. Проблема в том, что для основных типов int4, timestamp и т.п. изначально поддерживаются только btree, а для геометрических типов только gist индексы.

Создадим отдельный индекс по местоположению:
CREATE INDEX i_geodata_log__point
ON geodata_log
USING gist
(box(obj_point, obj_point));
План запроса:
Limit  (cost=256.03..256.04 rows=1 width=570) (actual time=17.081..17.082 rows=1 loops=1)
-> Sort (cost=256.03..256.04 rows=1 width=570) (actual time=17.079..17.079 rows=1 loops=1)
Sort Key: obj_created
Sort Method: top-N heapsort Memory: 17kB
-> Bitmap Heap Scan on geodata_log (cost=4.79..256.02 rows=1 width=570) (actual time=9.903..15.112 rows=829 loops=1)
Recheck Cond: (box(obj_point, obj_point) <@ '(55.450349029297,38.715576171875),(50.857639595549,30.337280273438)'::box)
Filter: (obj_created > '2008-07-04 00:00:00+04'::timestamp with time zone)
-> Bitmap Index Scan on i_geodata_log__point (cost=0.00..4.79 rows=67 width=0) (actual time=9.231..9.231 rows=2362 loops=1)
Index Cond: (box(obj_point, obj_point) <@ '(55.450349029297,38.715576171875),(50.857639595549,30.337280273438)'::box
Total runtime: 17.184 ms
И по времени фиксации:
CREATE INDEX i_geodata_log__obj_created
ON geodata_log
USING btree
(obj_created);
План:
Limit  (cost=40.44..40.45 rows=1 width=570) (actual time=15.288..15.288 rows=1 loops=1)
-> Sort (cost=40.44..40.45 rows=1 width=570) (actual time=15.285..15.285 rows=1 loops=1)
Sort Key: obj_created
Sort Method: top-N heapsort Memory: 17kB
-> Bitmap Heap Scan on geodata_log (cost=36.42..40.43 rows=1 width=570) (actual time=10.909..13.297 rows=829 loops=1)
Recheck Cond: ((box(obj_point, obj_point) <@ '(55.450349029297,38.715576171875),(50.857639595549,30.337280273438)'::box) AND (obj_created > '2008-07-04 00:00:00+04'::timestamp with time zone))
-> BitmapAnd (cost=36.42..36.42 rows=1 width=0) (actual time=10.751..10.751 rows=0 loops=1)
-> Bitmap Index Scan on i_geodata_log__point (cost=0.00..4.79 rows=67 width=0) (actual time=9.235..9.235 rows=2362 loops=1)
Index Cond: (box(obj_point, obj_point) <@ '(55.450349029297,38.715576171875),(50.857639595549,30.337280273438)'::box)
-> Bitmap Index Scan on i_geodata_log__obj_created (cost=0.00..31.38 rows=1483 width=0) (actual time=0.996..0.996 rows=3857 loops=1)
Index Cond: (obj_created > '2008-07-04 00:00:00+04'::timestamp with time zone)
Total runtime: 15.392 ms
Не смотря на индексные сканирования общая картина совсем не радует. Безвыходная ситуация? Нет.

Заглянув в стандартный набор сontrib'ов можно найти 2 интересных расширения
1. cube - добавляет тип cube - многомерный куб чисел с плавающей точкой
2. btree_gist - добавляет реализацию gist для int2, int4, timestamp и т.п.

cube может помочь ситуации, если представить наше пространство время тройками типа float (latitude, longitude, seconds_since_epoch), btree_gist просто позволит сделать смешанный индекс. Устанавливаем оба расширения, осталось проверить что быстрее.

Для cube добавим колонку area_time типа cube:
ALTER TABLE geodata_log
ADD COLUMN area_time cube;
Заполним её нашими точечными кубами (obj_point[0], obj_point[1], extract(epoch from obj_created)):
UPDATE geodata_log SET
area_time = cube(ARRAY[obj_point[0], obj_point[1], extract(epoch from obj_created)],
ARRAY[obj_point[0], obj_point[1], extract(epoch from obj_created)]);
И создадим индекс:
CREATE INDEX i_geodata_log__area_time
ON geodata_log
USING gist
(area_time);
Немного изменяем запрос и получаем план:
EXPLAIN ANALYZE
SELECT * FROM
geodata_log
WHERE
area_time <@ cube(ARRAY[50.857639595549, 30.337280273438, extract(epoch from '2008-07-04'::timestamp)],
ARRAY[55.450349029297, 38.715576171875, extract(epoch from now()::timestamp)])
ORDER BY
obj_created
LIMIT 1;

Limit (cost=265.36..265.36 rows=1 width=614) (actual time=5.312..5.313 rows=1 loops=1)
-> Sort (cost=265.36..265.52 rows=67 width=614) (actual time=5.310..5.310 rows=1 loops=1)
Sort Key: obj_created
Sort Method: top-N heapsort Memory: 17kB
-> Bitmap Heap Scan on geodata_log (cost=8.83..265.02 rows=67 width=614) (actual time=0.794..3.190 rows=829 loops=1)
Recheck Cond: (area_time <@ cube('{50.857639595549,30.337280273438,1215115200}'::double precision[], ARRAY[55.450349029297::double precision, 38.715576171875::double precision, date_part('epoch'::text, (now())::timestamp without time zone)]))
-> Bitmap Index Scan on i_geodata_log__area_time (cost=0.00..8.81 rows=67 width=0) (actual time=0.682..0.682 rows=829 loops=1)
Index Cond: (area_time <@ cube('{50.857639595549,30.337280273438,1215115200}'::double precision[], ARRAY[55.450349029297::double precision, 38.715576171875::double precision, date_part('epoch'::text, (now())::timestamp without time zone)]))
Total runtime: 5.420 ms
Уже лучше, определённо. Теперь пробуем btree_gist:
CREATE INDEX i_geodata_log__created_point
ON geodata_log
USING gist
(obj_created, box(obj_point, obj_point));

EXPLAIN ANALYZE
SELECT * FROM
geodata_log
WHERE
box(obj_point, obj_point) <@ box(point(50.857639595549, 30.337280273438), point(55.450349029297, 38.715576171875))
AND obj_created > '2008-07-04'
ORDER BY
obj_created
LIMIT 1;

Limit (cost=8.31..8.32 rows=1 width=570) (actual time=5.092..5.093 rows=1 loops=1)
-> Sort (cost=8.31..8.32 rows=1 width=570) (actual time=5.092..5.092 rows=1 loops=1)
Sort Key: obj_created
Sort Method: top-N heapsort Memory: 17kB
-> Index Scan using i_geodata_log__created_point on geodata_log (cost=0.00..8.30 rows=1 width=570) (actual time=0.076..3.408 rows=829 loops=1)
Index Cond: ((obj_created > '2008-07-04 00:00:00+04'::timestamp with time zone) AND (box(obj_point, obj_point) <@ '(55.450349029297,38.715576171875),(50.857639595549,30.337280273438)'::box))
Total runtime: 5.170 ms
Совсем хорошо.

Сделав несколько испытаний в различных ситуациях, я отметил, что разницы в производительности между cube и btree_gist на моих объёмах данных практически нет, но т.к. cube потребует создание дополнительной колонки и триггера, который будет её обновлять, я выбрал btree_gist.

Что лучше выбрать вам будет зависит от вашей ситуации.

p.s. В ближайшее время опубликую пост "Отделяем мух от котлет: проблема активных и архивных данных", будет интересно, обещаю :)

С уважением,
Серегей Коноплёв

September 8, 2008

Аналог \dT в SQL (mustread!!!)

Очень полезная вещь, я считаю...

Перевод SQL equivalent to \dT с pgsql-general

Bram Kuijper для pgsql-general

Всем привет,

Если я хочу получить список типов (т.е., типов данных или перечислений), то я могу выпонить комманду '\dT' в клиенте PostgreSQL.

Однако, я, похоже, не могу выяснить альтернативу \dT комманды в SQL, т.е. как бы я мог получить список типов с помошью SQL скрипта.

Напимер, если я создаю свой собственный ENUM:
CREATE TYPE bird AS ENUM('duck','goose');
то, быстро просмотрев information schema не просто обнаружить где храниться это перечисление. Является ли information schema правильным местом для поиска? Какое SQL выражение мне нужно для получения определённых пользователем типов?

заранее спасибо,
Bram Kuijper

Pavel Stehule для Bram, pgsql-general

Привет

Bram Kuijper:
> Всем привет,
>
> Если я хочу получить список типов (т.е., типов данных или перечислений),
> то я могу выпонить комманду '\dT' в клиенте PostgreSQL.
>

запустите psql с -E параметром. И увидите все SQL запросы используемые для метакомманд.
[pavel@localhost ~]$ psql -E postgres
psql (8.4devel)
Type "help" for help.

postgres=# \dT
********* QUERY **********
SELECT n.nspname as "Schema",
pg_catalog.format_type(t.oid, NULL) AS "Name",
pg_catalog.obj_description(t.oid, 'pg_type') as "Description"
FROM pg_catalog.pg_type t
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
WHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM
pg_catalog.pg_class c WHERE c.oid = t.typrelid))
AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid =
t.typelem AND el.typarray = t.oid)
AND pg_catalog.pg_type_is_visible(t.oid)
ORDER BY 1, 2;
**************************

List of data types
Schema | Name | Description
------------+-----------------------------+----------------------------
pg_catalog | abstime | absolute, limited-range date and time (Unix system time)
pg_catalog | aclitem | access control list
pg_catalog | "any" |
pg_catalog | anyarray |
pg_catalog | anyelement |

regards
Pavel Stehule

September 7, 2008

Проблема использования индексов с OFFSET LIMIT

Недавно вёл обсуждение касающееся pagination для очень большого количества записей (кстати, кое-кто обещал мне показать реализацию ;) и пришел к такому-же выводу...

Перевод Indexing problem with OFFSET LIMIT с pgsql-general

Oliver Weichhold для pgsql-general

Привет

У меня проблема в моём приложении и я не знаю как её решить.

Вот таблица и один из индексов:
CREATE TABLE foo
(
id serial NOT NULL,
foo_name character varying(100),
realm_id integer

... и ещё около 50 колонок
)

CREATE INDEX idx_foo_name_realm
ON foo
USING btree
(realm_id, foo_name);
В таблице foo около 8 миллионов строк.

Сама проблема:

Рассмотрим запрос:
SELECT * FROM foo WHERE realm_id = 228 order by foo_name LIMIT 200 OFFSET 15000
И его план выполнения:
Limit  (cost=57527.13..58294.16 rows=200 width=575) (actual time=182.302..184.971 rows=200 loops=1)
-> Index Scan using idx_foo_name_realm on foo (cost=0.00..62159.98 rows=16208 width=575) (actual time=0.085..166.861 rows=15200 loops=1)
Index Cond: (realm_id = 228)
Total runtime: 185.591 ms
А теперь рассмотрим этот:
SELECT * FROM foo WHERE realm_id = 228 order by foo_name LIMIT 200 OFFSET 15999
Limit  (cost=59601.92..59602.42 rows=200 width=575) (actual time=1069.759..1072.310 rows=200 loops=1)
-> Sort (cost=59561.92..59602.44 rows=16208 width=575) (actual time=929.948..1052.620 rows=16199 loops=1)
Sort Key: foo_name
Sort Method: external merge Disk: 8984kB
-> Bitmap Heap Scan on foo (cost=306.69..54270.62 rows=16208 width=575) (actual time=9.612..235.902 rows=21788 loops=1)
Recheck Cond: (realm_id = 228)
-> Bitmap Index Scan on foo_realm_id (cost=0.00..302.64 rows=16208 width=0) (actual time=8.733..8.733 rows=21810 loops=1)
Index Cond: (realm_id = 228)
Total runtime: 1084.706 ms
Время выполнения увеличилось в 10 раз из-за того, что postgres перестал использовать индексы.

Может кто-нибудь объяснить что происходит и как это обойти? Является ли это проблемой памяти?

Merlin Moncure для Oliver, pgsql-general

Oliver Weichhold:
> Hello
>
> I have problem in my applications and don't know how to fix it.
>
> ...

Попробуйте это:
SELECT * FROM foo WHERE realm_id = 228 order by realm_id, foo_name
LIMIT 200 OFFSET
15000
Или, что даже лучше, не используйте 'offset' вообще. Это просто не потребно. Если вы хотите пропустить 200 строк разом, сделайте это отдельным запросом:

1 раз:
select * from foo order by realm_id, foo_name limit 200;
после этого:
select * from foo where (realm_id, foo_name) > (last_realm_id,
last_foo_name) order by realm_id, foo_name limit 200;
и вы будете приятно удивлены :-). Это также будет более правильно по отношению к другим сессиям, которые делают операции добавления/удаления.

merlin

Tom Lane для Merlin, Oliver, pgsql-general

Merlin Moncure:
> Oliver Weichhold:
>> Рассмотрим запрос:
>>
>> SELECT * FROM foo WHERE realm_id = 228 order by foo_name LIMIT 200 OFFSET
>> 15000
> Попробуйте это:
>
> SELECT * FROM foo WHERE realm_id = 228 order by realm_id, foo_name
> LIMIT 200 OFFSET
> 15000
>
> Или, что даже лучше, не используйте 'offset' вообще. Это просто
> не потребно. Если вы хотите пропустить 200 строк разом, сделайте
> это отдельным запросом:

Да, большие значения OFFSET всегда вредны для производительности, т.к. пропуск этих строк конечно же не волшебство - бэкэнд должен прочесть их и потом откинуть. Избегайте OFFSET.

Отступив от темы хочу спросить - вы не рассматривали вариант с использованием курсоров?

regards, tom lane

В ожидании 8.4 - \ef в psql

Перевод Waiting for 8.4 - \ef in psql с select * from depesz;

Сегодня Tom Lane применил патч, написанный Abhijit Menon-Sen, который добавляет интересную возможность в psql. А именно - упрощает изменение определения функций.

Сообщение патча наиболее полно всё объясняет:
Реализация psql комманды "\ef" для редактирования определения функции.
В поддержку этого, создание backend-функции pg_get_functiondef().
Комманда функциональна, но, возможно, потребует небольших улучшений...

Abhijit Menon-Sen
Более подробно.

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

Другим способом было хранение creation.sql файла, и его изменение когда необходимо изменить функцию.

Но сейчас всё стало проще. Достаточно вызвать \ef function_name и предпочитаемый вами редактор ($EDITOR) откроется с полным "CREATE OR REPLACE FUNCTION..." на редактирование.

В случае, когда есть несколько функций с указанным именем, вы получите интересное сообщение об ошибке:
# \ef texticlike
ERROR: more than one function named "texticlike"
LINE 1: SELECT 'texticlike'::pg_catalog.regproc::pg_catalog.oid
^
И тогда вы сможете вызвать \ef с параметрами функции:
# \ef texticlike(citext, text)
Великолепное дополнение. psql становится всё лучше и лучше.

September 6, 2008

Охота на “idle in transactions”

Привет

Я извиняюсь за то, что вот уже несколько дней от меня нет ни строчки, но суета связанная с переездом в МСК отнимает практически всё время, плюс редко удаётся выйти в сеть. Обещаю в ближайшее время исправить ситуацию, а пока вот...

Перевод Hunting "idle in transactions" с select * from depesz;

Если вы когда-нибудь встречались с коннектами в “idle in transaction”, то вы скорее всего ненавидите их. Я знаю это точно, т.к. сам их ненавижу. Они мешают таким "класным штукам" как репликация, вакуум, DDL запросы.

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

Легко сказать, сложно сделать. Как же устранить проблему?

Я, конечно, мог бы убить все процессы postgres'а в сотоянии “idle in transaction”, но это не безопасное и, определённо, не оптимальное решение.

Лучшим решением было бы исправить те части кода, которые создают коннекты, ожидающие (простаивающие в) транзакции. Но как?

Прежде всего давайте зададим простой вопрос: что же такое соединение в “idle in transaction”?

Всё очень просто: это удерживающий соединение процесс postgres, который начал транзакцию, возможно что-то делал в её рамках, но сейчас ничего уже не делает, и транзакция всё ещё остаётся открытой ожидая COMMIT'а или ROLLBACK'а.

Проверка pg_stat_activity не скажет нам ни чего - соединение просто в ожидании. Мы, конечно, узнаем хост/порт коннекта, какую базу данных он использует и под каким пользователем. Но в большенстве случаев этого не достаточно. Ожидающие в транзакции соединения берут начало в серверах приложений и, не смотря на то что известно какое приложение послужило их причиной, не известно в какой его части искать ошибку.

К счастью, немного манипуляций на Perl, и мы сможем получить некоторую информацию.

Стачала настроим PostgreSQL так, что он будет логировать все запросы с точными "таймстэмпами" и длительностью выполнения запросов. Например, используя такие настройки:
            name            |     setting

----------------------------+-----------------

log_line_prefix | %m %u@%d %p %r

log_min_duration_statement | 0
Логи будут выглядеть так:
2008-08-28 16:58:31.978 CEST some_user@some_database 26001 10.3.5.1(45094) LOG:  duration: 1.387 ms  statement: SELECT ...
Интересующие части это "таймстэмп" вначале и длительность запроса.

Первое что надо знать, это то, что "таймстэмп" показывает время окончания выполнения запроса. Т.к. он равен '2008-08-28 16:58:31.978' и длительнось немного больше 1млс, то можно полагать, что время начала запроса '2008-08-28 16:58:31.977'.

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

Анализируя весь лог мы можем выяснить разницу между окончанием одного запроса и началом другого.

Основываясь на этом я написал программу, которая делает анализ и выводит интересующую информацию.

Вот таким образом:
=> zcat postgresql-2008-08-28_165831.log.gz | ./find-iit-connections.pl 2>/dev/null

162ms @ 2008-08-28 16:58:55.795 CEST (pid: 8570), queries:

- begin

- some_query

- other_query

- another

- the query *after* long idle in transaction
Она ищет периоды idle-in-transactions больше 100млс. Изменить время можно в строке:
my $MINIMAL_IDLE_TO_REPORT = 100;
Вывод отображает то как долго было ожидание в транзакции (162млс), в какое время оно закончилось, pid процесса, кторый был в этом состоянии, и список всех запросов (с начала транзакции) за которыми это последовало.

С этой информацией найти проблемный участок кода становится намного легче.

Комментарии

David Fetter, 30 августа 2008 в 14:14
Не было бы проще загрузить CVS логи (см. снизу траницы: http://www.postgresql.org/docs/current/static/runtime-config-logging.html) в аналитическую БД и выполнить SQL операцию над ними?
Cheers,
David.

depesz, 30 августа 2008 в 22:28
Определённо, но проблема в том, что баа данных, которую я имел ввиду, работает под 8.2., а CVS логи добавлены только в 8.3.

Robert Treat, 1 сентября 2008 в 15:00
На некоторых системах включение логирования запросов звучит довольно пугающе. Я время от времени пользуюсь dtrace для таких вещей, но когда речь идёт о IIT, надо быть довольно удачливым, чтобы получить всю верную информацию. Однако, одна вешь, которую можно сделать, это посмотреть на pg_locks для того, чтобы определить что блокируется IIT. Вместе с информацией о пользователе/хосте/приложении этого часто бывает достаточно для локализации той самой части приложения и без логирования всех запросов.

August 31, 2008

Маленький скрипт для отслеживания логов pg в реальном времени

Если логи вашего pg пишутся в /path/to/pg_log/dir/ и вы хотите отслеживать ошибки в реальном времени попробуйте этот маленький shell-скрипт
#!/bin/bash

cd /path/to/pg_log/dir/;
while true; do
clear;
cat `ls | tail -n 2` | grep ERROR | tail -n 100;
sleep 30;
done

Tiny script for real time pg_log tracking

If your pg-logs are writing to /path/to/pg_log/dir/ and you want to track ERRORs in real time try this tiny shell script
#!/bin/bash

cd /path/to/pg_log/dir/;
while true; do
clear;
cat `ls | tail -n 2` | grep ERROR | tail -n 100;
sleep 30;
done

Параметры по умолчанию для PL функций

Перевод default parameters for PL functions с Pavel Stehule's blog

Привет

Я закончил работу над одной из задач - значения по умолчанию для PL функций. Применение простое - такое же как значений по умолчанию в Firebird 2.x.
postgres=# create or replace function x1(int = 1,int = 2,int= 3)
returns int as $$
select $1+$2+$3;
$$ language sql;
CREATE FUNCTION
postgres=# select x1();
x1
----
6
(1 row)

postgres=# select x1(10);;
x1
----
15
(1 row)

postgres=# select x1(10,20);
x1
----
33
(1 row)

postgres=# select x1(10,20,30);
x1
----
60
(1 row)
Это первый шаг - и менее обсуждаемый. Второй шаг будет сложным - существует два мнения по поводу синтаксиса именованных параметров: вариант a) подобный Oracle синтаксис name => expression и вариант b) собственный синтаксис на основе "AS" expression AS name. Я предпочитаю вариант (a) - думаю это более читабельно (AS в SQL используется для меток). Вариант (b) надёжен с точки зрения совместимости. На pg_hackers было обсуждение без какого-либо заключения. Так что, я надеюсь, по крайней мере значения по умолчанию будут включены в проект.

пока
Pavel

August 26, 2008

My PostgreSQL related posts

In the nearest future I'm going to start posting about PostgreSQL in russian and in english on this blog. Russian posts in general will contain translations of the most interesting articles from PostgreSQL related blogs and decisions from mail lists. English posts will contain translations from russian. Also I'm going to post my friends and my thoughts around PostgreSQL.

Postgs in both languages will be able on this RSS feed, in english only on this, in russian on this.

Моя линейка постов на тему PostgreSQL

В ближайшее время я собираюсь начать линейку постов на тему PostgreSQL на русском и английском языках. Русские посты, в основной своей массе, будут содержать переводы показавшихся мне интересными статей из "около-постгрисовских" англоязычных блогов и решений из списков рассылки. Английские, соответственно, из русскоязычных. Ну и конечно на обоих языках буду публиковать свои мысли и мысли моих друзей на тему PostgreSQL.

Посты на обоих языках будут доступны на этом RSS канале, только русскоязычные посты на этом и только англоязычные на этом.

August 25, 2008

My friendfeed

http://friendfeed.com/grayhemp

August 22, 2008

МСК

Ок-ок, попробую ответить на вопросы "Ну и чего вы все туда рвётесь?", "Не понимаю как можно срываться с такого стабильного места?", "Зачем вам это надо?" и тому подобные.

Я наверное занимаю сейчас одну из самых высоких в КРР ниш для специалиста моего профиля, в сочетании карьеры, денег и морального удовлетворения. Тут я не могу не поблагодарить судьбу и людей, которые мне помогли этого достичь. Но последние пол года-год у меня складывается стойкое ощущение того, что я останавливаюсь в плане профессионального развития.

Проанализировав ситуацию я пришел к выводу, что для дальнейшего движения вперёд, находясь в КРР, мне остаётся либо менять профиль (может быть частично) для того чтобы сделать ещё пару маленьких шажков, либо идти в управление. В первом случае придётся потратить довольно много времени и не будет ощутимой финансовой выгоды. Во втором случае придётся пожертвовать навыками специалиста ради ощутимой начальной выгоды и в дальнейшем оставаться менеджером регионального уровня много лет в ожидании шанса выбиться в топ. Это то, что касается карьерной лесницы.

Касаемо же развития своего ИТ бизнеса, на сколько я понял, в МСК легче найти финансы и заинтересованность. Но это больше в перспективе.

В общем, точнее в душе эмоциональном плане, больше склоняюсь к ответу "Поедем посмотрим, может что хорошее получится. Энивэй, вернуться есть куда :)".

Как сделать облако тегов для Blogger.com

Вот такой вот способ предлагает автор для преобразования местных Labels в Tags Cloud.
UPD. Если возникнут проблемы с сохранением попробуйте скопировать код отсюда.

August 19, 2008

Hello Blogger!

Well, hello everyone! I'm starting my new blog here and going to let you into all important moments of my life.
p.s. My old one you can find at gray-hemp.livejournal.com