четверг, 31 декабря 2009 г.

Утилиты chiark

Анонс от Ian Jackson: chiark-tcl 1.0.0: cdb, adns, etc. for Tcl, plus journalling cdb

Что есть в ленни:
aptitude show libtcl-chiark-1 chiark-utils-bin chiark-scripts chiark-rwbuffer chiark-really chiark-backup

Пакет chiark-really:
Описание: really - a tool for gaining privilege (simple, realistic sudo)
really is a program that allows certain users to become whatever user they like on request. It is a bit like sudo in that respect. However, really is simpler than sudo, and doesn't give the system administrator any false security promises. So really is less of a general security risk to the system.

Unlike sudo it does not pretend that the called account can be any more secure than the calling account. so there is never a need for a password. If you wanted to restrict which commands and functions the called user can perform, use userv, not really or sudo.

Also unlike sudo, really only works if the calling user is supposed to be equivalent to root. But, really can also be used by root-equivalent users to become any user, not just root; in this way it can be a replacement for certain uses of su.


sudo делает все, чтобы компрометировать пароль пользователя, в то время как автор really об этом подумал.

Пакет chiark-scripts:
Описание: chiark system administration scripts
This package contains a number of small administration scripts used by chiark.greenend.org.uk and other systems belonging to the Sinister Greenend Organisation. Featuring:

chiark-named-conf: a tool for managing nameserver configurations and checking for suspected DNS problems. Its main functions are to check that delegations are appropriate and working, that secondary zones are slaved from the right places, and to generate a configuration for BIND, from its own input file.

sync-accounts: a simple but flexible account info synchroniser. sync-accounts is a tool for copying un*x account data from remote systems and installing it locally. It is flexible and reasonably straightforward, but lacks integration with other distributed databases such as NIS.

...

gnucap2genspic, ngspice2genspic, genspic2gnuplot: convert gnucap files and ngspice output files to genspic and genspic files to gnuplot input so they can be plotted.

hexterm: connects to serial port and allows the user interact in ASCII and hex. Ie, a hex "terminal" program which lets you speak a serial port protocol directly. (Needs tcl8.4 to be installed.)


Пакет: chiark-utils-bin:
Описание: chiark system administration utilities
This package contains a number of small administration scripts used by chiark.greenend.org.uk and other systems belonging to the Sinister Greenend Organisation. Currently featuring only:

with-lock-ex: a simple tool for acquiring a lockfile before running another program or script.
...


Пакет libtcl-chiark-1:
Описание: Tcl interfaces for adns, cdb, crypto, etc.
Tcl bindings for:
* adns (resolver library)
* cdb (constant database) plus journalling writeable database
* crypto: the nettle cryptographic library
* hbytes: bytestrings with hex as string representation but efficient


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

Тотальная деквалификация

Витус (Vitus Wagner) в блоге весьма точно подметил:
А каких экономистов не возьми, они родом из середины XIX века. Что марксисты, что австрийцы. Все что делали с тех пор, это накручивали на выдвинутые тогда идеи ту или иную математику.

Замечательно сформулировано! Оглядимся вокруг - везде идет то или иное накручивание, причем какое... Что касается математики, то это очень удобная ширма - почти никто не понимает, что это такое, но притом все уверены, что понимают. Как говорит Задорнов, математика давно стала сродни оккультным наукам (а самого Задорнова в СМИ уже то ли к шпионам КГБ причислили, то ли к масонам, в общем, сделали все для того, чтобы общество не понимающих Задорнова не чувствовало себя в дураках).

Вот как пример: спросим (можно у гугла или яндекса, можно у продавца в магазине - ответ практически идентичен), ноутбук подойдет для работы? Ага, вы уже догадались - в ответ узнаем о новых миллионах и миллиардах транзисторов (привлекает больных гигантоманией), новом уменьшенном технологическом процессе (правильно, не у всех гигантомания, у некоторых наоборот), новых технологиях защиты (неважно, кого и от чего, главное, шизофреников заставить купить),... Наиболее въедливые и сознательные доберутся до тестов... графических пакетов, времени запуска операционных систем и прочих животрепещущих подробностей. Но ответ на свой вопрос мы не получим - с точки зрения современного социума наш вопрос некорректен в принципе. И притом еще экономисты/политологи/ и прочие искусственно выведенные подвиды хомо сапиенс удивляются, а почему офисные работники в социальных сетях сидят, в аське, да в игры "режутся" - да ведь это как раз то, на что они запрограммированы и для чего, собственно, их "рабочие" компьютеры выпущены.

А что, если в сотовом салоне попросить аккумулятор для сотового телефона 6-ти летней давности? Нам попробуют всучить самый дешевый из имеющихся телефонов или начнут спрашивать, что такого в старом телефоне, чтобы не менять его на новый. Ну не их собачье дело это, зачем тот телефон, но ведь искренне не понимают, как так - пришедший к ним человек имеет возможность купить новый телефон, но не имеет никакого желания это делать. На вопрос, какие бы полезные функции могли предложить новые аппараты, начнут перечислять... да, именно, - игры. Серьезно, эти продавцы - они вообще вменяемые? Думаю, что нет, а точнее, это узкоспециализированные биороботы с определенной программой, и никакими вопросами их не сбить с этой программы.

Что же - вместо выпуска продукции, удовлетворяющей потребности потребителей, оказалось проще выпускать потребителей с потребностями, которые удовлетворяет имеющаяся продукция. Общество и технология совместно коллапсировали в некий самодостаточный кокон. Из такого состояния существуют только два пути - истощение внутренних ресурсов вследствии продолжающейся жизнедеятельности (самопожирание, ибо больше в коконе потреблять нечего) или внешнее воздействие (опять же нацеленное на остающиеся в коконе ресурсы). В обоих случаях стоит лишь заметить, что все живое в состоянии кокона беззащитно (известно, что кокон служит средством защиты от внешнего мира, освобождая таким образом резервы, ранее занятые этой самой защитой).

Не забудем и современное образование - СМИ оперируют понятиями "кадры", "молодые специалисты", "выпускники" и т.п. Знаете, в чем особенность этих слов? А в том, что они не являются определениями - их нельзя проверить или извлечь из них какую-то информацию. Вот что умеет "кадр"? Напрашивается игра слов - разве что "кадрить". Или "выпускник"? Для меня "выпускник" это какой-то выпускной вентиль на трубе, для спуска излишнего давления. Право слово, нехорошие ассоциации напрашиваются с канализационными трубами общества, откуда нечто "выпускается"... Фразеологизм "молодой специалист" весьма гаденько выглядит - вроде бы почти специалист, но гарантированно недоделанный, и даже неизвестно, станет ли когда-нибудь доделанным. На производстве это заготовкой спокон веков называли, а в системе образования придумали обтекаемую формулировку. Зато слова "инженер" СМИ старательно избегают - еще бы, ведь этот термин легко проверяется. Инженер умеет спроектировать мост, построить плотину, вырастить урожай, спроектировать и построить ЭВМ, и много чего еще - в зависимости от специальности, конечно, но инженер может повысить квалификацию или переквалифицироваться, потому что у него есть эта самая квалификация. Вот как раз факт тотального отсутствия квалификации и замалчивается. Я бы сказал, что налицо тотальная деквалификация общества. Для техногенного общества это - катастрофа. Но вместо принятия мер, происходит активное "накручивание" в виде громких заявлений о технопарках, инновационной экономике, информационном обществе и прочих вещах, которые современной России близки как звездолет папуасу.

Вместо послесловия. Область ИТ представляет для меня значительный интерес, потому решил о ней поговорить отдельно. Вскоре дополню здесь же.

Upd.

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

К сожалению, для большинства пользователей и разработчиков выбора как раз нет. Корпорации запугивают людей монстрообразными средами разработки и программными платформами, создавая впечатление, что это неотъемлемый атрибут творчества. Это все равно, что навязывать мольберт со встроенным холодильником, кондиционером, автомобилем, джакузи и проч., убеждая, что только с таким мольбертом можно стать настоящим художником. Между тем, в подобных монстрах концепции убоги, а просто реализуются только те вещи, которые были многократно реализованы ранее и стали банальны.

Вообще, языки программирования нужны лишь для того, чтобы как можно нагляднее и эффективнее реализовать решение требуемой задачи. Скорость выполнения кода, синтаксис, способ выполнения и все прочее вторично. Сравнивать скорость решения задач очень низкого уровня и требуемый обыем кода в строках бессмысленно, поскольку реальные задачи будут решаться в зависимости от их постановки человеком. А многие мегабайты или десятки мегабайт кода на любом языке (особенно это свойственно яве и пхп) это уже мусор, такой объем может быть необходим операционной системе [со множеством прикладных библиотек, файлами справки, конфигурационными файлами, примерами, etc.], но не одному приложению.

Ситуация парадоксальная - корпорации все активнее применяют открытые решения, но подавляющая биомасса, то есть масса разработчиков использует энтерпрайз решения, полагая, что это "круто". Люди никак не хотят осознать, что творчество есть выражение идей автора, и никакие инструменты не помогут создать что-то сверх того. Зато сложные инструменты могут помешать четко увидеть и воплотить идеи. Нужно-то всего ничего - взять кусок материала и отсечь все лишнее (почти цитата).

Пока что понимание вижу только у разработчиков ПО для микроконтроллеров и проч. оборудования, где требуется и высокая надежность, и малый объем выполняемого кода и [в наличии] много других ограничений.


Upd. Статья в тему: Образование: Страна непрофессионалов

среда, 30 декабря 2009 г.

qmail в быту

Из вики qmail:
qmail — MTA (агент доставки почты), который работает под Unix. Он был написан Daniel J. Bernstein как более безопасная замена для популярного MTA Sendmail.


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

Бестселлер Life with qmail
Написано "казенным" языком, что-то в духе "забивания гвоздя есть продукт соударения неподвижного гвоздя с движущимся орудием типа молоток". В общем, читать толку мало, ибо писать автор просто не умеет. См. ниже перевод "Жизнь с qmail" - один из немногих случаев, когда перевод лучше оригинала, читается легко, стиль написания на уровне статьи для популярного журнала.

Очень кратко, но со вкусом: Qmail On Debian

Бегло рассказано об установке в дебиане, удалении конфликтующих пакетов, настройке: Qmail Vpopmail Debian Wiki Для больших любителей извращений рассказано про связку с mysql, ну да ладно, простим автору.

А вот это занимательное чтиво расскажет о том, как сделать помойку из своего линукса, наставив кучу софта, скомпилированного руками, и создав множество директорий и файлов: How to install QMail Ага, здесь тоже есть про mysql, кто бы сомневался. Стоит смотреть на список модулей и доп. софта для работы с qmail, остальное игнорировать.

Здесь смотреть последние два раздела - настройка релея и хранения почты в "хомятниках": Running qmail on Debian Что касается замены системного sendmail, то это еще вопрос, нужен ли он, и ручное создание симлинка явно не выход - лучше переменные окружения подправить. Впрочем, вольному воля.

А вот и весьма вменяемый ресурс: Qmail: Русский информационный сайт Смотрим Жизнь с qmail и остальное по вкусу.

Теперь можно и официальную документацию осознать: FAQ.html и http://www.qmail.org/man/index.html

Upd.
Самая полезная и нужная команда оказалась следующая:

dpkg --force-depends --purge exim4 exim4-base exim4-config exim4-daemon-light

Выполняется весьма долго, т.к. удаляются миллионы созданных exim файлов сообщений. И что я его раньше не прибил...

Для qmail резонно вместо всех интерфейсов (параметр 0 в команде запуска tcpserver) указать нужный ip-адрес.

Upd.
Как выяснилось, патчи для работы с mysql/ldap накладывают горе-админы, не умеющие настроить регулярную выгрузку списка пользователей в cdb-файл, подхватываемый qmail. Вот интересно, каким местом эти самые админы думают, громоздя медленный доступ к удаленному хранилищу вместо быстрого к локальному cdb файлу? Радует одно - при "падении" пресловутого mysql эти умники получают свое сполна от пользователей и начальства (что, впрочем, вовсе их не вразумляет).

Upd.
Решил выделить команду создания почтового ящика для пользователя:

maildirmake $HOME/Maildir
echo ./Maildir > $HOME/.qmail

Соответственно, следует или вызывать указанную утилиту после выполнения adduser, или добавить нужный файл (файлы) в /etc/skel/:

cd /etc/skel
maildirmake Maildir
echo ./Maildir/ > .qmail

понедельник, 28 декабря 2009 г.

stunnel в мирных целях

Как в песне поется: "А мне всегда чего-то не хватает..." Вот в stunnel не хватает передачи информации о клиентском сертификате в хидерах. Наподобии вот такого: Patch info for setenv_mf.

Возможно, еще имеет смысл с OpenSSL его научить работать - тогда можно делать нормальное управление сертификатами (патчи есть, но не смотрел). Сейчас что-то как-то делается как описано по ссылкам Tips for doing client cert authentication и Using Certificates with Stunnel

суббота, 26 декабря 2009 г.

Updated sqlite3-rdiff

The new version of sqlite3-rdiff utility can produce really small signatures. See below test on 10 Mb database.



The analyze mode will help you to select optimal parameters for your databases.

пятница, 25 декабря 2009 г.

Мошенники сотовой связи

Недавно пришлось столкнуться с ситуацией - на телефон (не мой) с МТС-кой симкой пришла SMS с короткого номера 8353. Хозяйка телефона SMS проигнорировала, ответа не посылала (проверил исходящие на всякий случай, точно, на этот номер ничего не отсылалось). Внезапно баланс лицевого счета уходит в минус. Позвонив в контактный центр МТС по номеру 0890, узнаю - снято 270,93 руб. с НДС за якобы отправленную на этот номер SMS!

Привожу содержание SMS-сообщения:
http://www.telpics.ru/cache/animals/telpics_ru_653079223_119_119.jpg
Поддержка: http://only-support.com


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

Погуглив, обнаруживаю, что этот номер принадлежит контент-провайдеру Первый Альтернативный, который не то что подворовывает, а замечен с кистенем на большой дороге в темное и не очень время суток. Снова звоню в контактный центр, девочки-операторы настойчиво предлагают телефон контент-провайдера, на что я возмущенно отвечаю, что пользуюсь услугами МТС, никакого контент-провайдера не знаю и знать не хочу и в такой ситуации компания МТС должна позаботиться о своем клиенте. На вопрос, как я себе это представляю, отвечаю, что я хочу оформить жалобу на владельца короткого номера в МТС, который и будет далее проводить "разбор полетов" с контент-провайдером. Подействовало, переключили в финансовый отдел, где уже без лишних сложностей оформили жалобу и обещали разобраться и перезвонить в течении 5-ти дней (попросили назвать паспортные данные владельца номера в целях идентификации). Дело было в пятницу вечером, а в воскресенье ближе к вечеру действительно перезвонили и сообщили, что указанную выше сумму на счет вернули. Пообщавшись с позвонившим мне специалистом, узнал, что деньги возвращают независимо от того, была ли отправлена SMS или нет, т.к. это все равно мошенничество со стороны контент-провайдера.

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

Надеюсь, с вами и вашими близкими никогда не случится подобного, ну а если вдруг - отстоять свои права не сложно, для этого даже не обязательно вставать с любимого кресла/дивана/... (подставить нужное). Удачи!

Upd. Мошенники сотовой связи: продолжение

Pure-tcl sockets

Тесты fileevent socket-сервера на CoreQuad хосте для двух сценариев - возвращаемый документ 10 килобайт и 0,5 килобайт. Тестовый скрипт - немного модифицированный netserver.txt, возвращающий вместо эха http-заголовок 200 OK и простенькую html-страничку.

Ради интереса попробовал tclhttpd (оригинальный, 2004-го года) и его реинкарнацию wub. Первый выдает 40 TPS, а второй - 8 TPS. Комментарии, как говорится, излишни.

10k responce

$ openload -l 10 localhost:9999 200
URL: http://localhost:9999/
Clients: 200
Time Limit: 10 sec.
MaTps 6301.00, Tps 6301.00, Resp Time 0.020, Err 0%, Count 6301
MaTps 6324.30, Tps 6534.00, Resp Time 0.020, Err 0%, Count 12835
MaTps 6416.87, Tps 7250.00, Resp Time 0.018, Err 0%, Count 20085
MaTps 6434.88, Tps 6597.00, Resp Time 0.023, Err 0%, Count 26682
MaTps 6469.59, Tps 6782.00, Resp Time 0.019, Err 0%, Count 33464
MaTps 6501.94, Tps 6793.00, Resp Time 0.019, Err 0%, Count 40257
MaTps 6535.44, Tps 6837.00, Resp Time 0.030, Err 0%, Count 47094
MaTps 6563.40, Tps 6815.00, Resp Time 0.019, Err 0%, Count 53909
MaTps 6576.86, Tps 6698.00, Resp Time 0.019, Err 0%, Count 60607
MaTps 6595.97, Tps 6768.00, Resp Time 0.056, Err 0%, Count 67375
Total TPS: 6736.83
Avg. Response time: 0.024 sec.
Max Response time: 9.714 sec
Total Requests: 67375
Total Errors: 0


$ openload -l 10 localhost:9999 100
URL: http://localhost:9999/
Clients: 100
Time Limit: 10 sec.
MaTps 3251.00, Tps 3251.00, Resp Time 0.030, Err 0%, Count 3251
MaTps 3297.60, Tps 3717.00, Resp Time 0.027, Err 0%, Count 6968
MaTps 3417.44, Tps 4496.00, Resp Time 0.022, Err 0%, Count 11464
MaTps 3527.50, Tps 4518.00, Resp Time 0.022, Err 0%, Count 15982
MaTps 3672.85, Tps 4981.00, Resp Time 0.020, Err 0%, Count 20963
MaTps 3796.66, Tps 4911.00, Resp Time 0.020, Err 0%, Count 25874
MaTps 3843.60, Tps 4266.00, Resp Time 0.023, Err 0%, Count 30140
MaTps 3840.14, Tps 3809.00, Resp Time 0.026, Err 0%, Count 33949
MaTps 3800.42, Tps 3443.00, Resp Time 0.029, Err 0%, Count 37392
MaTps 3869.98, Tps 4496.00, Resp Time 0.022, Err 0%, Count 41888
Total TPS: 4188.38
Avg. Response time: 0.024 sec.
Max Response time: 0.090 sec
Total Requests: 41888
Total Errors: 0


$ openload -l 10 localhost:9999 50
URL: http://localhost:9999/
Clients: 50
Time Limit: 10 sec.
MaTps 4303.00, Tps 4303.00, Resp Time 0.012, Err 0%, Count 4303
MaTps 4317.90, Tps 4452.00, Resp Time 0.011, Err 0%, Count 8755
MaTps 4352.51, Tps 4664.00, Resp Time 0.011, Err 0%, Count 13419
MaTps 4379.56, Tps 4623.00, Resp Time 0.011, Err 0%, Count 18042
MaTps 4438.30, Tps 4967.00, Resp Time 0.010, Err 0%, Count 23009
MaTps 4467.67, Tps 4732.00, Resp Time 0.011, Err 0%, Count 27741
MaTps 4483.60, Tps 4627.00, Resp Time 0.011, Err 0%, Count 32368
MaTps 4476.54, Tps 4413.00, Resp Time 0.011, Err 0%, Count 36781
MaTps 4427.99, Tps 3991.00, Resp Time 0.013, Err 0%, Count 40772
MaTps 4383.79, Tps 3986.00, Resp Time 0.013, Err 0%, Count 44758
Total TPS: 4475.35
Avg. Response time: 0.011 sec.
Max Response time: 0.020 sec
Total Requests: 44758
Total Errors: 0



0.5k responce

$ sudo nice -n -20 openload -l 10 localhost:9999 1
URL: http://localhost:9999/
Clients: 1
Time Limit: 10 sec.
MaTps 4683.00, Tps 4683.00, Resp Time 0.000, Err 0%, Count 4683
MaTps 4676.60, Tps 4619.00, Resp Time 0.000, Err 0%, Count 9302
MaTps 4667.74, Tps 4588.00, Resp Time 0.000, Err 0%, Count 13890
MaTps 4652.97, Tps 4520.00, Resp Time 0.000, Err 0%, Count 18410
MaTps 4645.77, Tps 4581.00, Resp Time 0.000, Err 0%, Count 22991
MaTps 4641.79, Tps 4606.00, Resp Time 0.000, Err 0%, Count 27597
MaTps 4639.41, Tps 4618.00, Resp Time 0.000, Err 0%, Count 32215
MaTps 4634.67, Tps 4592.00, Resp Time 0.000, Err 0%, Count 36807
MaTps 4623.30, Tps 4521.00, Resp Time 0.000, Err 0%, Count 41328
MaTps 4610.87, Tps 4499.00, Resp Time 0.000, Err 0%, Count 45827
Total TPS: 4582.24
Avg. Response time: 0.000 sec.
Max Response time: 0.012 sec
Total Requests: 45827
Total Errors: 0


$ sudo nice -n -20 openload -l 10 localhost:9999 5
URL: http://localhost:9999/
Clients: 5
Time Limit: 10 sec.
MaTps 5097.00, Tps 5097.00, Resp Time 0.001, Err 0%, Count 5097
MaTps 5065.10, Tps 4778.00, Resp Time 0.001, Err 0%, Count 9875
MaTps 5053.59, Tps 4950.00, Resp Time 0.001, Err 0%, Count 14825
MaTps 5037.93, Tps 4897.00, Resp Time 0.001, Err 0%, Count 19722
MaTps 5018.14, Tps 4840.00, Resp Time 0.001, Err 0%, Count 24562
MaTps 4999.32, Tps 4830.00, Resp Time 0.001, Err 0%, Count 29392
MaTps 4978.49, Tps 4791.00, Resp Time 0.001, Err 0%, Count 34183
MaTps 4954.64, Tps 4740.00, Resp Time 0.001, Err 0%, Count 38923
MaTps 4939.98, Tps 4808.00, Resp Time 0.001, Err 0%, Count 43731
MaTps 4921.28, Tps 4753.00, Resp Time 0.001, Err 0%, Count 48484
Total TPS: 4847.92
Avg. Response time: 0.001 sec.
Max Response time: 0.023 sec
Total Requests: 48484
Total Errors: 0

$ sudo nice -n -20 openload -l 10 localhost:9999 20
URL: http://localhost:9999/
Clients: 20
Time Limit: 10 sec.
MaTps 5692.00, Tps 5692.00, Resp Time 0.004, Err 0%, Count 5692
MaTps 5577.90, Tps 4551.00, Resp Time 0.004, Err 0%, Count 10243
MaTps 5461.31, Tps 4412.00, Resp Time 0.005, Err 0%, Count 14655
MaTps 5365.98, Tps 4508.00, Resp Time 0.004, Err 0%, Count 19163
MaTps 5286.28, Tps 4569.00, Resp Time 0.004, Err 0%, Count 23732
MaTps 5204.55, Tps 4469.00, Resp Time 0.004, Err 0%, Count 28201
MaTps 5130.50, Tps 4464.00, Resp Time 0.004, Err 0%, Count 32665
MaTps 5049.65, Tps 4322.00, Resp Time 0.005, Err 0%, Count 36987
MaTps 5009.68, Tps 4650.00, Resp Time 0.004, Err 0%, Count 41637
MaTps 4974.02, Tps 4653.00, Resp Time 0.004, Err 0%, Count 46290
Total TPS: 4628.54
Avg. Response time: 0.004 sec.
Max Response time: 0.015 sec
Total Requests: 46290
Total Errors: 0


$ sudo nice -n -20 openload -l 10 localhost:9999 50
URL: http://localhost:9999/
Clients: 50
Time Limit: 10 sec.
MaTps 9530.00, Tps 9530.00, Resp Time 0.005, Err 0%, Count 9530
MaTps 9266.20, Tps 6892.00, Resp Time 0.007, Err 0%, Count 16422
MaTps 9140.08, Tps 8005.00, Resp Time 0.006, Err 0%, Count 24427
MaTps 8684.07, Tps 4580.00, Resp Time 0.011, Err 0%, Count 29007
MaTps 8299.37, Tps 4837.00, Resp Time 0.010, Err 0%, Count 33844
MaTps 7903.13, Tps 4337.00, Resp Time 0.011, Err 0%, Count 38181
MaTps 7608.42, Tps 4956.00, Resp Time 0.010, Err 0%, Count 43137
MaTps 7305.07, Tps 4575.00, Resp Time 0.011, Err 0%, Count 47712
MaTps 6936.37, Tps 3618.00, Resp Time 0.014, Err 0%, Count 51330
MaTps 7147.43, Tps 9047.00, Resp Time 0.006, Err 0%, Count 60377
Total TPS: 6037.10
Avg. Response time: 0.008 sec.
Max Response time: 0.018 sec
Total Requests: 60377
Total Errors: 0


$ sudo nice -n -20 openload -l 10 localhost:9999 100
URL: http://localhost:9999/
Clients: 100
Time Limit: 10 sec.
MaTps 7591.00, Tps 7591.00, Resp Time 0.013, Err 0%, Count 7591
MaTps 7788.30, Tps 9564.00, Resp Time 0.011, Err 0%, Count 17155
MaTps 7463.87, Tps 4544.00, Resp Time 0.022, Err 0%, Count 21699
MaTps 7545.68, Tps 8282.00, Resp Time 0.012, Err 0%, Count 29981
MaTps 7756.81, Tps 9657.00, Resp Time 0.010, Err 0%, Count 39638
MaTps 7940.53, Tps 9594.00, Resp Time 0.010, Err 0%, Count 49232
MaTps 8054.78, Tps 9083.00, Resp Time 0.011, Err 0%, Count 58315
MaTps 8205.70, Tps 9564.00, Resp Time 0.010, Err 0%, Count 67879
MaTps 8269.43, Tps 8843.00, Resp Time 0.011, Err 0%, Count 76722
MaTps 8303.19, Tps 8607.00, Resp Time 0.012, Err 0%, Count 85329
Total TPS: 8532.05
Avg. Response time: 0.012 sec.
Max Response time: 0.047 sec
Total Requests: 85329
Total Errors: 0



sudo nice -n -20 openload -l 10 localhost:9999 200
URL: http://localhost:9999/
Clients: 200
Time Limit: 10 sec.
MaTps 10043.00, Tps 10043.00, Resp Time 0.013, Err 0%, Count 10043
MaTps 10013.60, Tps 9749.00, Resp Time 0.013, Err 0%, Count 19792
MaTps 9922.24, Tps 9100.00, Resp Time 0.014, Err 0%, Count 28892
MaTps 9780.12, Tps 8501.00, Resp Time 0.022, Err 0%, Count 37393
MaTps 9309.32, Tps 5072.21, Resp Time 0.026, Err 0%, Count 42521
MaTps 8980.09, Tps 6017.00, Resp Time 0.022, Err 0%, Count 48538
MaTps 8850.68, Tps 7686.00, Resp Time 0.023, Err 0%, Count 56224
MaTps 8636.71, Tps 6711.00, Resp Time 0.023, Err 0%, Count 62935
MaTps 7965.04, Tps 1920.00, Resp Time 0.068, Err 0%, Count 64855
Total TPS: 6484.20
Avg. Response time: 0.020 sec.
Max Response time: 9.707 sec
Total Requests: 64855
Total Errors: 0


$ sudo nice -n -20 openload -l 10 localhost:9999 500
URL: http://localhost:9999/
Clients: 500
Time Limit: 10 sec.
MaTps 9850.00, Tps 9850.00, Resp Time 0.014, Err 0%, Count 9850
MaTps 9824.10, Tps 9591.00, Resp Time 0.014, Err 0%, Count 19441
MaTps 9835.89, Tps 9942.00, Resp Time 0.013, Err 0%, Count 29383
MaTps 9854.90, Tps 10026.00, Resp Time 0.043, Err 0%, Count 39409
MaTps 9862.01, Tps 9926.00, Resp Time 0.018, Err 0%, Count 49335
MaTps 9864.91, Tps 9891.00, Resp Time 0.013, Err 0%, Count 59226
MaTps 9864.12, Tps 9857.00, Resp Time 0.044, Err 0%, Count 69083
MaTps 9745.91, Tps 8682.00, Resp Time 0.023, Err 0%, Count 77765
MaTps 9602.42, Tps 8311.00, Resp Time 0.016, Err 0%, Count 86076
MaTps 9607.87, Tps 9657.00, Resp Time 0.119, Err 0%, Count 95733
Total TPS: 9572.34
Avg. Response time: 0.032 sec.
Max Response time: 9.584 sec
Total Requests: 95733
Total Errors: 37


$ sudo nice -n -20 openload -l 10 localhost:9999 1000
URL: http://localhost:9999/
Clients: 1000
Time Limit: 10 sec.
MaTps 4979.00, Tps 4979.00, Resp Time 0.027, Err 0%, Count 4979
MaTps 5039.50, Tps 5584.00, Resp Time 0.023, Err 0%, Count 10563
MaTps 5534.75, Tps 9992.00, Resp Time 0.013, Err 0%, Count 20555
MaTps 5973.48, Tps 9922.00, Resp Time 0.055, Err 0%, Count 30477
MaTps 6337.63, Tps 9615.00, Resp Time 0.027, Err 0%, Count 40092
MaTps 6638.56, Tps 9347.00, Resp Time 0.014, Err 0%, Count 49439
MaTps 6928.91, Tps 9542.00, Resp Time 0.068, Err 0%, Count 58981
MaTps 7134.02, Tps 8980.00, Resp Time 0.027, Err 0%, Count 67961
MaTps 7360.82, Tps 9402.00, Resp Time 0.014, Err 0%, Count 77363
MaTps 7592.63, Tps 9679.00, Resp Time 0.152, Err 0%, Count 87042
Total TPS: 8703.33
Avg. Response time: 0.044 sec.
Max Response time: 9.650 sec
Total Requests: 87042
Total Errors: 0

AOL Server API

В далекие времена, когда API AOL Server нельзя было просто взять и загрузить в обычный тиклевый интерпретатор, жил-был вот такой проект: nstcl Исходники можно посмотреть в репозитории здесь.

Начиная с версии 4.x, все намного проще:


$ tclsh
% load /usr/lib/aolserver4/lib/libnsd.so
% join [lsort [info commands ns_*]] "\n"
ns_addrbyhost
ns_adp_abort
ns_adp_append
ns_adp_argc
ns_adp_argv
ns_adp_bind_args
ns_adp_break
ns_adp_close
ns_adp_compress
ns_adp_ctl
ns_adp_debug
ns_adp_dir
ns_adp_dump
ns_adp_eval
ns_adp_exception
ns_adp_flush
ns_adp_ident
ns_adp_include
ns_adp_mime
ns_adp_mimetype
ns_adp_parse
ns_adp_puts
ns_adp_registeradp
ns_adp_registerproc
ns_adp_registerscript
ns_adp_registertag
ns_adp_return
ns_adp_safeeval
ns_adp_stats
ns_adp_stream
ns_adp_tell
ns_adp_trunc
ns_after
ns_atclose
ns_atexit
ns_atshutdown
ns_atsignal
ns_cache
ns_cache_flush
ns_cache_keys
ns_cache_names
ns_cache_size
ns_cache_stats
ns_cancel
ns_chan
ns_charsets
ns_checkurl
ns_chmod
ns_cond
ns_config
ns_configsection
ns_configsections
ns_conn
ns_conncptofp
ns_connsendfp
ns_cp
ns_cpfp
ns_critsec
ns_crypt
ns_driver
ns_encodingforcharset
ns_env
ns_event
ns_fmttime
ns_for
ns_foreach
ns_ftruncate
ns_geturl
ns_gifsize
ns_gmtime
ns_guesstype
ns_headers
ns_hostbyaddr
ns_hrefs
ns_http
ns_httptime
ns_ictl
ns_info
ns_internalredirect
ns_interp_ctl
ns_job
ns_jpegsize
ns_kill
ns_library
ns_limits
ns_link
ns_localtime
ns_log
ns_logctl
ns_logroll
ns_loop_ctl
ns_markfordelete
ns_mkdir
ns_mktemp
ns_modulepath
ns_mutex
ns_normalizepath
ns_parseheader
ns_parsehttptime
ns_parsequery
ns_pause
ns_pools
ns_purgefiles
ns_puts
ns_quotehtml
ns_rand
ns_register_adp
ns_register_adptag
ns_register_encoding
ns_register_fastpath
ns_register_filter
ns_register_proc
ns_register_trace
ns_rename
ns_requestauthorize
ns_respond
ns_resume
ns_return
ns_returnadminnotice
ns_returnbadrequest
ns_returnerror
ns_returnfile
ns_returnforbidden
ns_returnfp
ns_returnnotfound
ns_returnnotice
ns_returnredirect
ns_returnunauthorized
ns_rmdir
ns_rollfile
ns_rwlock
ns_schedule_daily
ns_schedule_proc
ns_schedule_weekly
ns_sema
ns_server
ns_set
ns_share
ns_shutdown
ns_sleep
ns_sockaccept
ns_sockblocking
ns_sockcallback
ns_sockcheck
ns_socketpair
ns_socklisten
ns_socklistencallback
ns_socknonblocking
ns_socknread
ns_sockopen
ns_sockselect
ns_startcontent
ns_striphtml
ns_symlink
ns_thread
ns_time
ns_tmpnam
ns_truncate
ns_unlink
ns_unregister_adp
ns_unregister_proc
ns_unschedule_proc
ns_url2file
ns_urldecode
ns_urlencode
ns_uudecode
ns_uuencode
ns_var
ns_while
ns_write
ns_writecontent
ns_writefp

понедельник, 14 декабря 2009 г.

The small signature for sqlite3-rdiff

Depends: current test version of sqlite3-rdiff

The sqlite3-rdiff utility produces a signature file of size about 10% of original database size. Yes, it's better than coping an entire database but it's not good enough for production use. So I wrote a new algorithm that can build the small signatures. The algorithm calculates checksums for set of rows and so with N rows in each set the signature size will be decreased by the factor of N! Of course the delta file size will be increased but only a little for most databases.

My code uses a hack


select rowid/N as rowid, sum(murmurhash('$unixepoch',$cols)) from ... group by rowid


This solution is not good and may be used for testing only. This code will fail with N>16.


$ time ./sqlite3-rdiff signature slave.db slave.db.signature
signature slave.db slave.db.signature --table-name % --rows-per-hash 1
=7 system_config
=160 center
=7 role
=1 macroregion
=988 point
=7 region
=1841 user
=2867166 document_status
=25 comment
=250985 document_comment
=0 operation
=16 datatype
=7 type_template
=51 template
=370 template_field
=511335 document
=1593416 document_field
=757015 document_file
=7336 document_photo
=16 status_switch
=491129 message
=7 system_config_history
=160 center_history
=7 role_history
=1 macroregion_history
=988 point_history
=7 region_history
=1841 user_history
=2869992 document_status_history
=25 comment_history
=251191 document_comment_history
=0 operation_history
=16 datatype_history
=7 type_template_history
=51 template_history
=370 template_field_history
=511335 document_history
=1593416 document_field_history
=757015 document_file_history
=7336 document_photo_history
=16 status_switch_history

real 0m55.309s
user 0m53.075s
sys 0m1.444s


$ time ./sqlite3-rdiff delta slave.db.signature master.db slave.db.delta
delta slave.db.signature master.db slave.db.delta --table-name % --rows-per-hash 1
-0 +0 system_config
-0 +0 center
-0 +0 role
-0 +0 macroregion
-0 +0 point
-0 +0 region
-0 +0 user
-687 +2922 document_status
-0 +0 comment
-50 +230 document_comment
-0 +0 operation
-0 +0 datatype
-0 +0 type_template
-0 +0 template
-0 +0 template_field
-0 +550 document
-0 +1348 document_field
-0 +970 document_file
-0 +2 document_photo
-0 +0 status_switch
-0 +4443 message
-0 +0 system_config_history
-0 +0 center_history
-0 +0 role_history
-0 +0 macroregion_history
-0 +0 point_history
-0 +0 region_history
-0 +0 user_history
-0 +4202 document_status_history
-0 +0 comment_history
-0 +230 document_comment_history
-0 +0 operation_history
-0 +0 datatype_history
-0 +0 type_template_history
-0 +0 template_history
-0 +0 template_field_history
-0 +550 document_history
-0 +1348 document_field_history
-0 +970 document_file_history
-0 +2 document_photo_history
-0 +0 status_switch_history

real 4m20.282s
user 3m18.276s
sys 0m5.944s



$ time ./sqlite3-rdiff --rows-per-hash 16 signatureN slave.db slave.db.signature16
signature slave.db slave.db.signature16 --table-name % --rows-per-hash 16
=1 system_config
=19 center
=1 role
=1 macroregion
=90 point
=1 region
=185 user
=179198 document_status
=3 comment
=15687 document_comment
=0 operation
=2 datatype
=1 type_template
=10 template
=57 template_field
=45270 document
=227657 document_field
=63430 document_file
=495 document_photo
=3 status_switch
=30697 message
=1 system_config_history
=11 center_history
=1 role_history
=1 macroregion_history
=62 point_history
=1 region_history
=116 user_history
=179375 document_status_history
=2 comment_history
=15700 document_comment_history
=0 operation_history
=2 datatype_history
=1 type_template_history
=4 template_history
=24 template_field_history
=31959 document_history
=99589 document_field_history
=47314 document_file_history
=459 document_photo_history
=2 status_switch_history

real 3m40.539s
user 1m40.978s
sys 0m8.097s


$ time ./sqlite3-rdiff --rows-per-hash 16 deltaN slave.db.signature16 master.db slave.db.delta16
delta slave.db.signature16 master.db slave.db.delta16 --table-name % --rows-per-hash 16
-0 +0 system_config
-0 +0 center
-0 +0 role
-0 +0 macroregion
-0 +0 point
-0 +0 region
-0 +0 user
-5808 +8042 document_status
-0 +0 comment
-480 +654 document_comment
-0 +0 operation
-0 +0 datatype
-0 +0 type_template
-0 +0 template
-0 +0 template_field
-16 +553 document
-16 +1352 document_field
-16 +974 document_file
-4 +4 document_photo
-0 +0 status_switch
-16 +4452 message
-0 +0 system_config_history
-0 +0 center_history
-0 +0 role_history
-0 +0 macroregion_history
-0 +0 point_history
-0 +0 region_history
-0 +0 user_history
-16 +4211 document_status_history
-0 +0 comment_history
-16 +238 document_comment_history
-0 +0 operation_history
-0 +0 datatype_history
-0 +0 type_template_history
-0 +0 template_history
-0 +0 template_field_history
-16 +558 document_history
-16 +1357 document_field_history
-16 +978 document_file_history
-11 +11 document_photo_history
-0 +0 status_switch_history

real 3m28.369s
user 1m51.347s
sys 0m7.928s



$ ls -lh|grep slave.db|awk '{print $5 "\t" $8}'
1.5G slave.db
3.4M slave.db.delta
4.3M slave.db.delta16
160M slave.db.signature
13M slave.db.signature16

суббота, 12 декабря 2009 г.

sqlite3-rdiff: master-slave replication for SQLite

Link: http://mobigroup.ru/files/sqlite-ext/sqlite3-rdiff

Depends: tcl 8.5, sqlite3, murmurhash SQLite extension

I'm glad to annonce the sqlite3-diff utility for SQLite replication. Are used the ROWID value as unique key of row and murmurhash for build signatures for each row. It's enough for master-slave replication. The INTEGER PRIMARY KEY is not mandatory becouse sqlite3-rdiff may store the ROWID values.

The signature and delta files are valid SQLite3 databases too. They can be dumped/restored, updated and analyzed by any SQLite3 client application.

Hash collisions are resolved by using unique salt for each signature. So after first sync the theoretical frequency of collision (when any record is changed but the hash is same) is about 10^-9, after second sync - 10^-18, after N syncs - 10^-N*9. The salt is unix epoch time which is saved by using "PRAGMA user_version" in signature and delta databases.

Note: the master-master replication is now unsupported becouse is can't be so universal and are needed some conflict-resolution strategies. But it possible with uuid extension to have really unique identifier for each record and use it as key for master-master replication (and of course the ROWID values must be ignored). But the signature and delta files will be too big.

Usage

NAME

sqlite3-rdiff - compute and apply signature-based row differences for SQLite3 databases

SYNOPSYS
sqlite3-rdiff [options] signature old-file signature-file

sqlite3-rdiff [options] delta signature-file new-file delta-file

sqlite3-rdiff [options] patch old-file delta-file result-file


DESCRIPTION

Signature is solving by murmurhash2 algorithm for full row. Hash collision is resolved by using unique salt for each replication session.

OPTIONS:

--table-name regular expression for table names, use ? as any single symbol and % for any set of symbols.

AUTHOR
Alexey Pechnikov <pechnikov@mobigroup.ru>

SEE ALSO
The main rdiff-backup web page is at http://geomapx.blogspot.com/2009/12/sqlite3-rdiff-master-slave-replication.html.


Example.

The test on my Core2 Duo laptop.


# add test triggers to block table inserts
# the replication utility may disable triggers when patching database
$ sqlite3 2009-11-01.db "create trigger telephony_log_i before insert on telephony_log \
begin select raise(ABORT,'trigger fire');end"

# make slave database with some changes
$ cp 2009-11-01.db 2009-11-01.db.slave
$ sqlite3 2009-11-01.db.slave "delete from telephony_log where rowid%100000=0;"
$ sqlite3 2009-11-01.db.slave "delete from telephony_log_error where rowid%10=0;"

# how many rows are deleted in previous commands?
$ sqlite3 2009-11-01.db "select count(*) from telephony_log where rowid%100000=0;"
14
$ sqlite3 2009-11-01.db "select count(*) from telephony_log_error where rowid%10=0;"
41


$ time ./sqlite3-rdiff signature 2009-11-01.db.slave 2009-11-01.db.signature
signature 2009-11-01.db.slave 2009-11-01.db.signature --table-name %
=1465379 telephony_log
=303709 telephony_log_rating
=377 telephony_log_error
=140 telephony_log_counter

real 0m40.361s
user 0m28.850s
sys 0m0.936s


$ time ./sqlite3-rdiff delta 2009-11-01.db.signature 2009-11-01.db 2009-11-01.db.delta
delta 2009-11-01.db.signature 2009-11-01.db 2009-11-01.db.delta --table-name %
-0 +14 telephony_log
-0 +0 telephony_log_rating
-0 +41 telephony_log_error
-0 +0 telephony_log_counter

real 1m50.502s
user 1m34.338s
sys 0m1.268s


$ time ./sqlite3-rdiff patch 2009-11-01.db.slave 2009-11-01.db.delta 2009-11-01.db.replicated
patch 2009-11-01.db.slave 2009-11-01.db.delta 2009-11-01.db.replicated --table-name %
-0 +14 telephony_log (2 triggers disabled)
-0 +0 telephony_log_rating (0 triggers disabled)
-0 +41 telephony_log_error (0 triggers disabled)
-0 +0 telephony_log_counter (0 triggers disabled)

real 0m24.624s
user 0m0.168s
sys 0m2.764s

$ ls -lh|grep 2009-11-01.db|awk '{print $5 "\t" $9}'
342M 2009-11-01.db
44K 2009-11-01.db.delta
342M 2009-11-01.db.replicated
23M 2009-11-01.db.signature
342M 2009-11-01.db.slave


We can check the success of replication by starting replication again. There are no new changes in replicated database and so the we may get the empty patch.


$ ./sqlite3-rdiff signature 2009-11-01.db.replicated 2009-11-01.db.signature2
signature 2009-11-01.db.replicated 2009-11-01.db.signature2 --table-name %
=1465393 telephony_log
=303709 telephony_log_rating
=418 telephony_log_error
=140 telephony_log_counter

$ ./sqlite3-rdiff delta 2009-11-01.db.signature2 2009-11-01.db 2009-11-01.db.delta2
delta 2009-11-01.db.signature2 2009-11-01.db 2009-11-01.db.delta2 --table-name %
-0 +0 telephony_log
-0 +0 telephony_log_rating
-0 +0 telephony_log_error
-0 +0 telephony_log_counter

$ ./sqlite3-rdiff patch 2009-11-01.db.replicated 2009-11-01.db.delta2 2009-11-01.db.replicated2
patch 2009-11-01.db.replicated 2009-11-01.db.delta2 2009-11-01.db.replicated2 --table-name %
-0 +0 telephony_log (1 triggers disabled)
-0 +0 telephony_log_rating (0 triggers disabled)
-0 +0 telephony_log_error (0 triggers disabled)
-0 +0 telephony_log_counter (0 triggers disabled)


All right, the patch is empty.

Upd.

The test on the Core Quad desktop with single 10 000 rpm HDD. Database work.db.copy is used for some tests and has different schema but the sqlite3-rdiff work fine on it.


$ time ./sqlite3-rdiff signature work.db.copy work.db.signature
signature work.db.copy work.db.signature --table-name %
=7 system_config
=160 center
=7 role
=1 macroregion
=988 point
=7 region
=1841 user
=2863845 document_status
=25 comment
=250561 document_comment
=0 operation
=16 datatype
=7 type_template
=51 template
=370 template_field
=510515 document
=1591502 document_field
=755659 document_file
=7329 document_photo
=16 status_switch
=486684 message
=7 system_config_history
=160 center_history
=7 role_history
=1 macroregion_history
=988 point_history
=7 region_history
=1841 user_history
=2863845 document_status_history
=25 comment_history
=250561 document_comment_history
=0 operation_history
=16 datatype_history
=7 type_template_history
=51 template_history
=370 template_field_history
=510515 document_history
=1591502 document_field_history
=755659 document_file_history
=7329 document_photo_history
=16 status_switch_history

real 1m58.328s
user 0m53.179s
sys 0m4.604s


$ time ./sqlite3-rdiff delta work.db.signature work.db work.db.delta
delta work.db.signature work.db work.db.delta --table-name %
-0 +0 system_config
-0 +0 center
-0 +0 role
-0 +0 macroregion
-0 +0 point
-0 +0 region
-0 +0 user
-995 +4316 document_status
-0 +0 comment
-205 +629 document_comment
-0 +0 operation
-0 +0 datatype
-0 +0 type_template
-0 +0 template
-0 +0 template_field
-0 +820 document
-0 +1914 document_field
-0 +1356 document_file
-0 +7 document_photo
-0 +0 status_switch
-0 +4445 message
-0 +0 system_config_history
-0 +0 center_history
-0 +0 role_history
-0 +0 macroregion_history
-0 +0 point_history
-0 +0 region_history
-0 +0 user_history
-0 +6147 document_status_history
-0 +0 comment_history
-0 +630 document_comment_history
-0 +0 operation_history
-0 +0 datatype_history
-0 +0 type_template_history
-0 +0 template_history
-0 +0 template_field_history
-0 +820 document_history
-0 +1914 document_field_history
-0 +1356 document_file_history
-0 +7 document_photo_history
-0 +0 status_switch_history

real 5m11.592s
user 3m23.989s
sys 0m5.704s


$ time ./sqlite3-rdiff patch work.db.copy work.db.delta work.db.copy
patch work.db.copy work.db.delta work.db.copy --table-name %
-0 +0 system_config (3 triggers disabled)
-0 +0 center (3 triggers disabled)
-0 +0 role (3 triggers disabled)
-0 +0 macroregion (3 triggers disabled)
-0 +0 point (3 triggers disabled)
-0 +0 region (3 triggers disabled)
-0 +0 user (3 triggers disabled)
-995 +4316 document_status (3 triggers disabled)
-0 +0 comment (3 triggers disabled)
-205 +629 document_comment (3 triggers disabled)
-0 +0 operation (3 triggers disabled)
-0 +0 datatype (3 triggers disabled)
-0 +0 type_template (3 triggers disabled)
-0 +0 template (3 triggers disabled)
-0 +0 template_field (3 triggers disabled)
-0 +820 document (3 triggers disabled)
-0 +1914 document_field (3 triggers disabled)
-0 +1356 document_file (3 triggers disabled)
-0 +7 document_photo (3 triggers disabled)
-0 +0 status_switch (3 triggers disabled)
-0 +4445 message (0 triggers disabled)
system_config_versioning
center_versioning
role_versioning
macroregion_versioning
point_versioning
region_versioning
user_versioning
document_status_versioning
comment_versioning
document_comment_versioning
operation_versioning
datatype_versioning
type_template_versioning
template_versioning
template_field_versioning
document_versioning
document_field_versioning
document_file_versioning
document_photo_versioning
status_switch_versioning

real 0m0.726s
user 0m0.500s
sys 0m0.124s


$ ls -lh|grep work.db|awk '{print $5 "\t" $8}'
1.5G work.db
2.0G work.db.copy
4.0M work.db.delta
159M work.db.signature

четверг, 10 декабря 2009 г.

The SQLite History extension

The article is not completed!

The History SQLite extension may be used for table history logging.

This extension has been tested with SQLite versions 3.6.x running under Linux Debian.

The code is public domain.

Link: http://mobigroup.ru/files/sqlite-ext/history/

Suggests: ENV extension

Usage

There are anly 2 functions in the extension:

history(table ,column) - add history logging support for the table [table] by key column [column].
unhistory(table) - drop history logging triggers for the table [table]. The history table is saved! Only triggers will be drops.

The *_history table consists all fields of the [table] table without any checks or constraints and some additional fields:

CREATE TABLE [table]_history(
...
_date REAL,
_action TEXT,
_user TEXT,
_host TEXT
);

The _action field has action names where 'I' - insert, 'U' - update, 'D' - delete and '' - initial import.

The index for common queries to history table is built autimatically:

sqlite> explain query plan select * from sessions_history where key='test key 0' order by _date;
0|0|TABLE sessions_history WITH INDEX sessions_history_complex_idx ORDER BY


Note: the last record version does not have duplicate in the history table. So "insert" operations does not do logging but all "update" and "delete" operations do logged.

example.sql

g$ sqlite3
SQLite version 3.6.21
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .load './libsqliteversioning.so'

sqlite>
.load './libsqlitehistory.so'

CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY,
key text not null unique on conflict replace,
value text not null
);
insert into sessions (key,value) values ('test key','test value');
select history('sessions', 'key'); -- create history table and triggers
select history('sessions', 'key'); -- no action
insert into sessions (key,value) values ('test key','test value');
.schema

insert into sessions (key,value) values ('test key 0','test value 0');
insert into sessions (key,value) values ('test key 1','test value 1');
insert into sessions (key,value) values ('test key 2','test value 2');

.header on
select * from sessions_history;

update sessions set value='new value' where key='test key 0';
delete from sessions where key='test key 0';
select datetime(_date),* from sessions_history where key='test key 0' order by _date desc;

.header off
explain query plan select * from sessions_history where key='test key 0' order by _date;
select unhistory('sessions');
.schema


Example results

sqlite> .read example.sql

CREATE TABLE sessions (
id INTEGER PRIMARY KEY,
key text not null unique on conflict replace,
value text not null
);
CREATE TABLE sessions_history(
id INT,
"key" TEXT,
value TEXT,
_date REAL,
_action TEXT,
_user TEXT,
_host TEXT
);
CREATE INDEX sessions_history_complex_idx on sessions_history(key, _date);
CREATE TRIGGER sessions_history_d BEFORE DELETE ON sessions
BEGIN INSERT INTO sessions_history SELECT *, julianday('now'), 'D', sqlite_user(),
sqlite_host() FROM sessions WHERE rowid=OLD.rowid;
END;
CREATE TRIGGER sessions_history_i AFTER INSERT ON sessions
BEGIN INSERT INTO sessions_history (key, _date, _action, _user, _host)
VALUES (NEW.key, julianday('now'), 'I', sqlite_user(), sqlite_host());
END;
CREATE TRIGGER sessions_history_u BEFORE UPDATE ON sessions BEGIN
SELECT RAISE (ABORT,'The key field "key" can not be modified') WHERE NEW.key != OLD.key;
INSERT INTO sessions_history SELECT *, julianday('now'), 'U', sqlite_user(),
sqlite_host() FROM sessions WHERE rowid=OLD.rowid;
END;
id|key|value|_date|_action|_user|_host
|test key||2455178.31008024|||
1|test key|test value|2455178.31008028|D||
|test key||2455178.31008028|I||
|test key 0||2455178.31008029|I||
|test key 1||2455178.31008029|I||
|test key 2||2455178.31008029|I||
datetime(_date)|id|key|value|_date|_action|_user|_host
2009-12-12 19:26:30|3|test key 0|new value|2455178.3100803|D||
2009-12-12 19:26:30|3|test key 0|test value 0|2455178.3100803|U||
2009-12-12 19:26:30||test key 0||2455178.31008029|I||
0|0|TABLE sessions_history WITH INDEX sessions_history_complex_idx ORDER BY

CREATE TABLE sessions (
id INTEGER PRIMARY KEY,
key text not null unique on conflict replace,
value text not null
);
CREATE TABLE sessions_history(
id INT,
"key" TEXT,
value TEXT,
_date REAL,
_action TEXT,
_user TEXT,
_host TEXT
);
CREATE INDEX sessions_history_complex_idx on sessions_history(key, _date);


Saving the host and user information
The SQLite is commonly used as embedded database engine and has no host and user access control mechanisms. But we can get this information from environment variables or by call of application functions. These two variants are described below.

ENV extension provided sqlite_* functions
The Env extension provide access to enviromnent variables, current user information, etc.

$ SQLITE_USER=USERNAME SQLITE_HOST=HOSTNAME sqlite3
SQLite version 3.6.21
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> select sqlite_user();
USERNAME
sqlite> select sqlite_host();
HOSTNAME


Application-defined sqlite_* functions

# install functions to db
proc ::dataset::install {} {
foreach func {sqlite_user sqlite_host} {
$db function $func [namespace current]::$func
}
}
proc ::dataset::sqlite_user {} {
return [ns_user id]
}
proc ::dataset::sqlite_host {} {
return [ns_conn server]
}

::dataset::eval db :memory: {
ns_html::puts [db onecolumn {select sqlite_user()}]
ns_html::br
ns_html::puts [db onecolumn {select sqlite_host()}]
}


TODO

1. Multikey support
versioning(table ,column1, column2, ...)

2. No-key support
versioning(table)

О master-master репликации

Нежданно-негаданно встретилась ссылка на следующий проект: rubyrep - репликация для PostgreSQL и MySQL.

Непосредственно реализацию не смотрел, а вот дизайн системы мне понравился. А вот насчет master-master режима есть разумное сомнение - разработчики предлагают некоторые алгоритмы разрешения конфликтов, но, по сути, если архитектура реплицируемой базы позволяет возникновение конфликтов, это проблема архитектуры, а отнюдь не репликатора. Вместо того, чтобы изменять одну и ту же таблицу на разных хостах, можно создать набор схем (schema) так, что каждый хост работает со своей схемой, тогда при репликации каждый хост будет мастером для "своей" схемы и слэйвом для "чужих". В эскулайте то же самое реализуется созданием набора баз данных (притом часть идентификаторов должны иметь или сквозную нумерацию, или быть уникальными uuid).

среда, 9 декабря 2009 г.

Трэйсер запросов в tclsqlite

В новом релизе 3.6.21 появилась возможность полноценной трассировки запросов (раньше значения забинденных переменных не выводились). Тест у меня для этой функции давно написан, его и привожу.

Кстати, нюанс - функция трассировки имеет возможность отменить выполнение запроса, так что аудит запросов в эскулайт легко делается встроенными средствами. Замечу, что инструкции PRAGMA не выводятся.


$ ./test_trace.tcl
{DROP TABLE IF EXISTS events}
{CREATE TABLE events (id INTEGER PRIMARY KEY,value INTEGER)}
{SAVEPOINT _tcl_transaction}
{insert into events (value) values (0)}
{insert into events (value) values (1)}
{insert into events (value) values (2)}
{insert into events (value) values (3)}
{insert into events (value) values (4)}
{insert into events (value) values (5)}
{insert into events (value) values (6)}
{insert into events (value) values (7)}
{insert into events (value) values (8)}
{insert into events (value) values (9)}
{insert into events (value) values ('value')}
COMMIT
{CREATE INDEX events_value_idx ON events(value);}
{select value from events}


test_trace.tcl

#!/usr/bin/tclsh8.5
package require sqlite3
sqlite3 db trace.db

proc trace {args} {
puts "$args"
return SQLITE_OK
}
db trace trace
db eval {PRAGMA legacy_file_format = off}
db eval {DROP TABLE IF EXISTS events}
db eval {CREATE TABLE events (id INTEGER PRIMARY KEY,value INTEGER)}
db transaction {
for {set i 0} {$i<10} {incr i} {
set value [expr {$i % 500000}]
db eval {insert into events (value) values ($value)}
}
set value value
db eval {insert into events (value) values ($value)}
}
db eval {CREATE INDEX events_value_idx ON events(value);}
db onecolumn {select value from events}
db close


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


ANALYZE
{SELECT idx, stat FROM 'main'.sqlite_stat1}
{SELECT idx,sampleno,sample FROM 'main'.sqlite_stat2}
VACUUM
{ATTACH '' AS vacuum_db;}
{BEGIN EXCLUSIVE;}
{SELECT 'CREATE TABLE vacuum_db.' || substr(sql,14) FROM sqlite_master WHERE type='table'
AND name!='sqlite_sequence' AND rootpage>0}
{CREATE TABLE vacuum_db.events (id INTEGER PRIMARY KEY,value INTEGER)}
{CREATE TABLE vacuum_db.sqlite_stat1(tbl,idx,stat)}
{CREATE TABLE vacuum_db.sqlite_stat2(tbl,idx,sampleno,sample)}
{SELECT 'CREATE INDEX vacuum_db.' || substr(sql,14) FROM sqlite_master WHERE sql LIKE
'CREATE INDEX %' }
{CREATE INDEX vacuum_db.events_value_idx ON events(value)}
{SELECT 'CREATE UNIQUE INDEX vacuum_db.' || substr(sql,21) FROM sqlite_master WHERE sql
LIKE 'CREATE UNIQUE INDEX %'}
{SELECT 'INSERT INTO vacuum_db.' || quote(name) || ' SELECT * FROM main.' || quote(name) ||
';'FROM main.sqlite_master WHERE type = 'table' AND name!='sqlite_sequence' AND rootpage>0}
{INSERT INTO vacuum_db.'events' SELECT * FROM main.'events';}
{INSERT INTO vacuum_db.'sqlite_stat1' SELECT * FROM main.'sqlite_stat1';}
{INSERT INTO vacuum_db.'sqlite_stat2' SELECT * FROM main.'sqlite_stat2';}
{SELECT 'DELETE FROM vacuum_db.' || quote(name) || ';' FROM vacuum_db.sqlite_master WHERE
name='sqlite_sequence' }
{SELECT 'INSERT INTO vacuum_db.' || quote(name) || ' SELECT * FROM main.' || quote(name) ||
';' FROM vacuum_db.sqlite_master WHERE name=='sqlite_sequence';}
{INSERT INTO vacuum_db.sqlite_master SELECT type, name, tbl_name, rootpage, sql FROM
main.sqlite_master WHERE type='view' OR type='trigger' OR (type='table' AND rootpage=0)}


As you can see internal system queries are visualized too. It's may be useful for developers but very bad for users becouse it's impossible to log only user-applied queries.

The test script:

#!/usr/bin/tclsh8.5
package require sqlite3
sqlite3 db trace.db

proc trace {args} {
puts "$args"
return SQLITE_OK
}
db trace trace
db eval {ANALYZE}
db eval {VACUUM}
db close


Upd.
In sqlite-users maillist Roger Binns answer by my question about universal mechanism for logging user queries:


> I want to build a universal extension for logging all user queries. I can write
> this with trace function but it's difficult or impossible to split system and
> user queries.

There are two ways that can be done. The first is to require your extension
and its API be used at which you can then filter and log as needed.

The way I would go about it is not using a SQLite extension, but instead
provide a shared library with logging functions. You can ensure the shared
library is loaded before SQLite (use LD_PRELOAD on Linux/Unix and something
similar on Windows) and hook the SQLite APIs. This requires no change to
the application.

For apswtrace I can output all SQL and bindings, rows returned, how long
each query took, most popular queries, queries that took the longest
(individually and ones that have the same text) etc. This doesn't require
any code changes in the Python application.

This unobtrusive mechanism also allows you to get more detailed. For
example you can track how often SQLite is busy (install a busy handler,
transparently call the user one if needed). You can even use it to fake
things. For example historically this mechanism has been used to fake the
current time, but you could fake busy results to help with testing. If you
also intercept read and write system calls you can even correspond I/O
activity to the queries that cause it. You can even capture stack
backtraces when tying them to SQL calls. There are so many possibilities.

Системы очередей для взаимодействия приложений

Собственно, идея состоит в реализации парадигмы вопрос-ответ. Очевидны два случаяя - ответ получаем сразу или когда-нибудь потом. Как пример, синхронная система - http, асинхронная - imap.

Вики излагает более подробно, см.
Message queue

Есть продакшен реализации и стандарты, например,
Red Hat Enterprise MRG
Этот монстр реализует стандарт AMQP. Замечу, что спецификация стандарта содержит почти 300 страниц.
В дебиане есть эрланг-реализация этого стандарта в пакете rabbitmq-server. Разработчики проекта RabbitMQ обещают надежность 9 девяток, что означает не более 3-х секунд недоступности системы в столетие, во что ни один здравомыслящий человек не поверит. К примеру, сто лет назад компьютеров еще не было, а через сто лет и таком проекте давно забудут. Так что мы обратимся к более адекватным идеям и реализацим.

Вот реализация попроще и без гарантии доставки сообщения: Gearman
Иллюстрации красивые и понятные, а вот архитектура системы подкачала: вместо стандартизированного протокола - наслоения API. Ну, пусть сами с ними и работают.

Так, картина более-менее прояснилась, посмотрим теперь, что есть для AOLServer. Модули посмотрим здесь и еще кое-где.
Нижеследующая реализация основана на встроенной функции ns_http, которая, признаться, документирована лишь частично, так что пример ее использования будет не лишним.
Nsxmlrpc
Сама реализация здесь: xmlrpc.tcl

Про Digital City разговор отдельный, потому ограничусь ссылкой на презентацию Tcl in AOL Digital City The Architecture of a Multithreaded High-Performance Web Site
В рамках этого проекта также есть модуль очереди: Networked Small Object Broker (nsob)

Вероятно, что-то найдется и для NaviServer, но я не смотрел.

Теперь к вопросу об интеграции AOL Server с внешней системой, в частности, qmail. Рекомендуемый источник познания, к которому следует припасть для работы с qmail, это Life with qmail.

Непосредственно связка AOL + qmail упоминается вот где:
Installing WebMail ACS 3.3
ArsDigita Server Architecture Auditing

Резюме: задачи на асинхронную обработку могут передаваться средствами qmail, а для синхронной подойдет функция ns_http из AOL Server с методом POST (без xmlrpc и других наворотов, разумеется).

Плюсы: используются стандартные протоколы и утилиты.

Минусы: оверхед на взаимодействие. Но необходимость передачи более чем тысяч сообщений в секунду между узлами одного приложения может возникнуть только при очень паршивой архитектуре. А трата миллисекунды времени на запуск задачи, выполняемой час, вполне допустима.

Upd.

tcl-mq: POSIX Message Queues for Tcl. (Tcl package)

понедельник, 7 декабря 2009 г.

MurmurHash 2.0 for SQLite

Нашел довольно удачную реализацию хэш-функции:
MurmurHash 2.0

Кстати, немного теории:
Hash Functions

Приведенный ниже вариант функции нужен для вычисления хэша сразу всех (или некоторых) полей записи, таким образом, можно вычислить хэши всех записей для целей репликации, хранения истории изменений (версионности) и других. Также можно использовать как компактный индекс на больших таблицах, где обычный многоколоночный индекс непримением по соображениям производительности.

Sqlite3 murmurhash extension

воскресенье, 6 декабря 2009 г.

SQLite Virtual Tables

Решил провести экспедицию в пучины иинтернет в поисках идей. А искать собираюсь различные виртуальные таблицы для SQLite. Virtual Table в SQLite - это такая штука, которая обеспечивает доступ к любым внешним или внутренним данным с помощью стандартного SQL, подробнее см. по ссылке:
The Virtual Table Mechanism Of SQLite

Если мы заглянем в вики-страничку на офсайте
http://www.sqlite.org/cvstrac/wiki?p=VirtualTables
то в самом низу увидим некую приписку о существовании модуля для создания виртуальных таблиц на языке perl. Несложно найти и пример:
SQLite::VirtualTable::Pivot

Что ж, начало положено, идем дальше. Следующая реализация, судя по описанию, весьма функциональна, но является частью большого проекта - libferris:

Libferris and SQLite--A Powerful Combination, Part 1
Libferris and SQLite--A Powerful Combination, Part 2
Libferris and SQLite--A Powerful Combination, Part 3

Обещан доступ к xml-файлам, структуре файлов и каталогов (и их свойствам), к базам PostgreSQL, и многим другим хранилищам информации. Проект написан на C++ и имеет целую кучу зависимостей, так что пока не компилировал. Вводная статья от автора: Filesystem Indexing with libferris А вот и еще: Search file servers from the Web with libferris and PHP

А вот и просто модуль доступа к файловой системе, в зависимостях значится только Apache Portable Runtime Library:
Query Anything with SQLite: Virtual tables are the foundation of powerful feature

Еще встретилось расширение для чтения лент новостей Atom:
SQLite table to read Atom feeds
Оно собралось без особых усилий, хотя для новых версий gcc потребовалось немного подправить.
Пример использования:

.load /tmp/libfeedme.so.0.0.0
create virtual table onlamp using feedme
('http://mysqlmusings.blogspot.com/feeds/posts/default');
select title from onlamp;

SQLite table to read Atom feeds
...
Replication of DELETE FROM versus TRUNCATE TABLE
Row-based replication for the future
Row-based replication and user defined functions

.header on
.mode line
select * from onlamp limit 1;
title = Bisection testing using Quilt
link = http://mysqlmusings.blogspot.com/2009/11/bisection-testing-using-quilt.html
id = tag:blogger.com,1999:blog-23496029.post-7467363913754324621
updated = 2009-11-17T12:13:13.610+01:00
author = Mats Kindahlhttp://www.blogger.com/profile/07528917029894926261noreply@blogger.com
summary =



Upd.
Using libferris with XML
The World Is a libferris Filesystem

пятница, 4 декабря 2009 г.

Тестирование PostgreSQL 8.3 на больших таблицах: 40М записей и ограничение ОЗУ

Продолжение статьи

Тестирование PostgreSQL 8.3 на больших таблицах: денормализация (40М записей).

В предыдущих заметках мы выполняли некоторые типичные выборки из таблиц с десятками миллионов записей в разных версиях PostgreSQL. Пожалуй, такие таблицы и в самом деле можно назвать большими. Но можно ли назвать нашу тестовую БД большой? Разумеется, нет. Нельзя потому, что вся наша БД умещалась не только в ОЗУ сервера, но даже и в буфере памяти PostgreSQL (shared memory). Да, объемы памяти растут, но растут и массивы информации, которые мы хотим обрабатывать, потому для работы с действительно большими БД мы должны быть готовы к тому, что Бд будет значительно больше, чем объем доступной памяти. Для того, чтобы получить удобные для сравнения результаты, мы воспользуемся той же тестовой БД, но объем памяти на запрос и для кэша уменьшим вдесятеро и теперь наша таблица вдесятеро превышает размер кэша постгреса. Для того, чтобы обнспечить работу с "горячим" кэшем (когда нужные данные закешированы), мы предварительно проводим запрос "select (*) ...", и только потом делаем "explain analyze ..." для того же запроса; такой подход гарантирует нам, что полученное время выполнения наименьшее из возможных. Также перед началом работы мы выполним команду "analyze".

Параметры сервера PostgreSQL:

shared_buffers = 160MB
work_mem = 10MB # min 64kB
maintenance_work_mem = 20MB # min 1MB


Нормализованная таблица без справочников (40M записей)

select count(*) from tmp_facts7 where a1=2 and a2=2 and a3=2 and a4=2;
count
-------
391
(1 row)

explain analyze select * from tmp_facts7 where a1=2 and a2=2 and a3=2 and a4=2;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on tmp_facts7 (cost=102337.80..122391.31 rows=303 width=16) (actual time=7236.866..9634.813 rows=391 loops=1)
Recheck Cond: ((a3 = 2) AND (a4 = 2) AND (a2 = 2))
Filter: (a1 = 2)
-> BitmapAnd (cost=102337.80..102337.80 rows=5749 width=0) (actual time=7225.881..7225.881 rows=0 loops=1)
-> Bitmap Index Scan on tmp_facts7_a3_idx (cost=0.00..33790.48 rows=2075563 width=0) (actual time=2366.257..2366.257 rows=2223561 loops=1)
Index Cond: (a3 = 2)
-> Bitmap Index Scan on tmp_facts7_a4_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=2367.129..2367.129 rows=2224826 loops=1)
Index Cond: (a4 = 2)
-> Bitmap Index Scan on tmp_facts7_a2_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=2365.304..2365.304 rows=2221344 loops=1)
Index Cond: (a2 = 2)
Total runtime: 9635.849 ms
(11 rows)


Нормализованная таблица со справочниками (40M записей + 10,100,1k,10k записей в справочниках)

select count(*)
from tmp_facts7
join tmp_facts7_a1 on tmp_facts7.a1=tmp_facts7_a1.id
join tmp_facts7_a2 on tmp_facts7.a2=tmp_facts7_a2.id
join tmp_facts7_a3 on tmp_facts7.a3=tmp_facts7_a3.id
join tmp_facts7_a4 on tmp_facts7.a4=tmp_facts7_a4.id
where tmp_facts7_a1.name='00' and tmp_facts7_a2.name='00' and tmp_facts7_a3.name='00' and tmp_facts7_a4.name='00';
count
-------
391
(1 row)

explain analyze
select count(*)
from tmp_facts7
join tmp_facts7_a1 on tmp_facts7.a1=tmp_facts7_a1.id
join tmp_facts7_a2 on tmp_facts7.a2=tmp_facts7_a2.id
join tmp_facts7_a3 on tmp_facts7.a3=tmp_facts7_a3.id
join tmp_facts7_a4 on tmp_facts7.a4=tmp_facts7_a4.id
where tmp_facts7_a1.name='00' and tmp_facts7_a2.name='00' and tmp_facts7_a3.name='00' and tmp_facts7_a4.name='00';
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=124783.94..124783.95 rows=1 width=0) (actual time=9909.587..9909.588 rows=1 loops=1)
-> Nested Loop (cost=104399.33..124783.94 rows=1 width=0) (actual time=7236.545..9909.143 rows=391 loops=1)
Join Filter: (tmp_facts7.a1 = tmp_facts7_a1.id)
-> Nested Loop (cost=104399.33..124782.80 rows=1 width=4) (actual time=7226.662..9871.196 rows=6768 loops=1)
-> Nested Loop (cost=0.00..19.81 rows=1 width=12) (actual time=0.105..0.141 rows=1 loops=1)
-> Nested Loop (cost=0.00..11.53 rows=1 width=8) (actual time=0.079..0.110 rows=1 loops=1)
-> Seq Scan on tmp_facts7_a2 (cost=0.00..3.25 rows=1 width=4) (actual time=0.011..0.039 rows=1 loops=1)
Filter: (name = '00'::text)
-> Index Scan using tmp_facts7_a4_name_idx on tmp_facts7_a4 (cost=0.00..8.27 rows=1 width=4) (actual time=0.065..0.066 rows=1 loops=1)
Index Cond: (tmp_facts7_a4.name = '00'::text)
-> Index Scan using tmp_facts7_a3_name_idx on tmp_facts7_a3 (cost=0.00..8.27 rows=1 width=4) (actual time=0.026..0.031 rows=1 loops=1)
Index Cond: (tmp_facts7_a3.name = '00'::text)
-> Bitmap Heap Scan on tmp_facts7 (cost=104399.33..124660.93 rows=5832 width=16) (actual time=7226.553..9865.072 rows=6768 loops=1)
Recheck Cond: ((tmp_facts7.a4 = tmp_facts7_a4.id) AND (tmp_facts7.a3 = tmp_facts7_a3.id) AND (tmp_facts7.a2 = tmp_facts7_a2.id))
-> BitmapAnd (cost=104399.33..104399.33 rows=5832 width=0) (actual time=7224.425..7224.425 rows=0 loops=1)
-> Bitmap Index Scan on tmp_facts7_a4_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=2366.752..2366.752 rows=2224826 loops=1)
Index Cond: (tmp_facts7.a4 = tmp_facts7_a4.id)
-> Bitmap Index Scan on tmp_facts7_a3_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=2367.871..2367.871 rows=2223561 loops=1)
Index Cond: (tmp_facts7.a3 = tmp_facts7_a3.id)
-> Bitmap Index Scan on tmp_facts7_a2_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=2363.284..2363.284 rows=2221344 loops=1)
Index Cond: (tmp_facts7.a2 = tmp_facts7_a2.id)
-> Seq Scan on tmp_facts7_a1 (cost=0.00..1.12 rows=1 width=4) (actual time=0.002..0.004 rows=1 loops=6768)
Filter: (tmp_facts7_a1.name = '00'::text)
Total runtime: 9910.388 ms
(24 rows)


select count(*)
from tmp_facts7
where
tmp_facts7.a1 IN (select id from tmp_facts7_a1 where name='00') and
tmp_facts7.a2 IN (select id from tmp_facts7_a2 where name='00') and
tmp_facts7.a3 IN (select id from tmp_facts7_a3 where name='00') and
tmp_facts7.a4 IN (select id from tmp_facts7_a4 where name='00');
count
-------
391
(1 row)

explain analyze
select count(*)
from tmp_facts7
where
tmp_facts7.a1 IN (select id from tmp_facts7_a1 where name='00') and
tmp_facts7.a2 IN (select id from tmp_facts7_a2 where name='00') and
tmp_facts7.a3 IN (select id from tmp_facts7_a3 where name='00') and
tmp_facts7.a4 IN (select id from tmp_facts7_a4 where name='00');
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=298421.25..298421.26 rows=1 width=0) (actual time=10406.970..10406.970 rows=1 loops=1)
-> Nested Loop IN Join (cost=34817.31..298421.24 rows=1 width=0) (actual time=2474.674..10406.279 rows=391 loops=1)
Join Filter: (tmp_facts7.a2 = tmp_facts7_a2.id)
-> Hash IN Join (cost=34817.31..298417.98 rows=1 width=4) (actual time=2445.150..10262.455 rows=6898 loops=1)
Hash Cond: (tmp_facts7.a1 = tmp_facts7_a1.id)
-> Hash IN Join (cost=34816.17..298416.82 rows=4 width=8) (actual time=2444.948..10200.662 rows=124045 loops=1)
Hash Cond: (tmp_facts7.a3 = tmp_facts7_a3.id)
-> Nested Loop (cost=34807.89..298393.49 rows=4000 width=12) (actual time=2444.714..9127.511 rows=2224826 loops=1)
-> HashAggregate (cost=8.28..8.29 rows=1 width=4) (actual time=0.067..0.068 rows=1 loops=1)
-> Index Scan using tmp_facts7_a4_name_idx on tmp_facts7_a4 (cost=0.00..8.27 rows=1 width=4) (actual time=0.062..0.063 rows=1 loops=1)
Index Cond: (name = '00'::text)
-> Bitmap Heap Scan on tmp_facts7 (cost=34799.61..272069.32 rows=2105271 width=16) (actual time=2444.642..7280.484 rows=2224826 loops=1)
Recheck Cond: (tmp_facts7.a4 = tmp_facts7_a4.id)
-> Bitmap Index Scan on tmp_facts7_a4_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=2372.121..2372.121 rows=2224826 loops=1)
Index Cond: (tmp_facts7.a4 = tmp_facts7_a4.id)
-> Hash (cost=8.27..8.27 rows=1 width=4) (actual time=0.051..0.051 rows=1 loops=1)
-> Index Scan using tmp_facts7_a3_name_idx on tmp_facts7_a3 (cost=0.00..8.27 rows=1 width=4) (actual time=0.047..0.048 rows=1 loops=1)
Index Cond: (name = '00'::text)
-> Hash (cost=1.12..1.12 rows=1 width=4) (actual time=0.025..0.025 rows=1 loops=1)
-> Seq Scan on tmp_facts7_a1 (cost=0.00..1.12 rows=1 width=4) (actual time=0.020..0.023 rows=1 loops=1)
Filter: (name = '00'::text)
-> Seq Scan on tmp_facts7_a2 (cost=0.00..3.25 rows=1 width=4) (actual time=0.003..0.019 rows=1 loops=6898)
Filter: (tmp_facts7_a2.name = '00'::text)
Total runtime: 10407.798 ms
(24 rows)


select count(*)
from tmp_facts7,tmp_facts7_a1,tmp_facts7_a2,tmp_facts7_a3,tmp_facts7_a4
where tmp_facts7.a1=tmp_facts7_a1.id and tmp_facts7.a2=tmp_facts7_a2.id and tmp_facts7.a3=tmp_facts7_a3.id and tmp_facts7.a4=tmp_facts7_a4.id
and tmp_facts7_a1.name='00' and tmp_facts7_a2.name='00' and tmp_facts7_a3.name='00' and tmp_facts7_a4.name='00';
count
-------
391
(1 row)

explain analyze
select count(*)
from tmp_facts7,tmp_facts7_a1,tmp_facts7_a2,tmp_facts7_a3,tmp_facts7_a4
where tmp_facts7.a1=tmp_facts7_a1.id and tmp_facts7.a2=tmp_facts7_a2.id and tmp_facts7.a3=tmp_facts7_a3.id and tmp_facts7.a4=tmp_facts7_a4.id
and tmp_facts7_a1.name='00' and tmp_facts7_a2.name='00' and tmp_facts7_a3.name='00' and tmp_facts7_a4.name='00';
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=124783.94..124783.95 rows=1 width=0) (actual time=9890.832..9890.832 rows=1 loops=1)
-> Nested Loop (cost=104399.33..124783.94 rows=1 width=0) (actual time=7238.444..9890.397 rows=391 loops=1)
Join Filter: (tmp_facts7.a1 = tmp_facts7_a1.id)
-> Nested Loop (cost=104399.33..124782.80 rows=1 width=4) (actual time=7228.591..9852.343 rows=6768 loops=1)
-> Nested Loop (cost=0.00..19.81 rows=1 width=12) (actual time=0.104..0.141 rows=1 loops=1)
-> Nested Loop (cost=0.00..11.53 rows=1 width=8) (actual time=0.077..0.109 rows=1 loops=1)
-> Seq Scan on tmp_facts7_a2 (cost=0.00..3.25 rows=1 width=4) (actual time=0.011..0.040 rows=1 loops=1)
Filter: (name = '00'::text)
-> Index Scan using tmp_facts7_a4_name_idx on tmp_facts7_a4 (cost=0.00..8.27 rows=1 width=4) (actual time=0.064..0.065 rows=1 loops=1)
Index Cond: (tmp_facts7_a4.name = '00'::text)
-> Index Scan using tmp_facts7_a3_name_idx on tmp_facts7_a3 (cost=0.00..8.27 rows=1 width=4) (actual time=0.025..0.028 rows=1 loops=1)
Index Cond: (tmp_facts7_a3.name = '00'::text)
-> Bitmap Heap Scan on tmp_facts7 (cost=104399.33..124660.93 rows=5832 width=16) (actual time=7228.484..9846.196 rows=6768 loops=1)
Recheck Cond: ((tmp_facts7.a4 = tmp_facts7_a4.id) AND (tmp_facts7.a3 = tmp_facts7_a3.id) AND (tmp_facts7.a2 = tmp_facts7_a2.id))
-> BitmapAnd (cost=104399.33..104399.33 rows=5832 width=0) (actual time=7226.363..7226.363 rows=0 loops=1)
-> Bitmap Index Scan on tmp_facts7_a4_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=2365.662..2365.662 rows=2224826 loops=1)
Index Cond: (tmp_facts7.a4 = tmp_facts7_a4.id)
-> Bitmap Index Scan on tmp_facts7_a3_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=2370.292..2370.292 rows=2223561 loops=1)
Index Cond: (tmp_facts7.a3 = tmp_facts7_a3.id)
-> Bitmap Index Scan on tmp_facts7_a2_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=2363.790..2363.790 rows=2221344 loops=1)
Index Cond: (tmp_facts7.a2 = tmp_facts7_a2.id)
-> Seq Scan on tmp_facts7_a1 (cost=0.00..1.12 rows=1 width=4) (actual time=0.002..0.004 rows=1 loops=6768)
Filter: (tmp_facts7_a1.name = '00'::text)
Total runtime: 9891.620 ms
(24 rows)


Выводы

Что ж, пришла пора подвести итог нашему путешествию по миру баз данных PostgreSQL. Обсудим по отдельности следующие моменты:

1. Работа с небольшими БД, которые заведомо помещаются в кэш СУБД.

В версии 8.1 имеются большие проблемы планировщика (про ввод-вывод, проматывание счетчика читающих транзакций и т.п. говорить не будем, т.к. это выходит за рамки проведенных тестов). В версии 8.3 сделаны серьезные улучшения, теперь за разумное время можно осуществлять выборки из таблиц в миллионы и десятки миллионов записей. Отметим, что при группировке и работе с view есть другие проблемы, но это опять же нужно исследовать отдельно. Таким образом, СУБД обеспечивает хранилище данных с удобным доступом, а вот что касается обработки данных, то для получения приемлемой производительности многое придется это делать в хранимых процедурах или в приложении, поскольку планировщик зачастую не способен обеспечить эффективное выполнение даже достаточно простых запросов.

Значит, возможно использовать СУБД для создания хранилища данных, но не для систем анализа.

2. Работа с большими БД, которые заведомо не помещаются в кэш СУБД.

В настоящий момент следует сказать "нет" для большинства применений. СУБД не способна группировать и агрегировать данные, не поместив их предварительно в кэш, так что все требуемые выборке данные будут размещаться в кэше и только после этого обрабатываться. Более того, даже уже находящиеся в кэше данные могут обрабатываться чрезвычайно медленно, как показано выше. И если последнее может быть когда-нибудь исправлено, то первое является основой архитектуры СУБД (и, кстати, может быть очень эффективно для выполнения множества запросов, которым требуются одни и те же данные).

Внешняя система кэширования позволит во многом исправить ситуацию, но получится весьма громоздкое решение, к тому же, по производительности уступающее аналогичной системе на файловых СУБД berkeleydb или sqlite, которые способны делать настолько быстрые выборки, что дополнительное кэширование становится просто бесполезным. Соответствующие тесты для SQLite уже опубликованы (вероятно, в дальнейшем к ним добавятся и новые тесты).

Тестирование PostgreSQL 8.3 на больших таблицах: 40М записей

Продолжение статьи

Тестирование PostgreSQL 8.3 на больших таблицах: денормализация (10М записей). Параметры сервера и PostgreSQL те же.

Нормализованная таблица без справочников (40M записей)

select count(*) from tmp_facts7 where a1=2 and a2=2 and a3=2 and a4=2;
count
-------
391
(1 row)

explain analyze select * from tmp_facts7 where a1=2 and a2=2 and a3=2 and a4=2;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on tmp_facts7 (cost=102820.61..123140.53 rows=307 width=16) (actual time=1240.778..1250.728 rows=391 loops=1)
Recheck Cond: ((a4 = 2) AND (a3 = 2) AND (a2 = 2))
Filter: (a1 = 2)
-> BitmapAnd (cost=102820.61..102820.61 rows=5832 width=0) (actual time=1238.241..1238.241 rows=0 loops=1)
-> Bitmap Index Scan on tmp_facts7_a4_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=371.444..371.444 rows=2224826 loops=1)
Index Cond: (a4 = 2)
-> Bitmap Index Scan on tmp_facts7_a3_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=371.881..371.881 rows=2223561 loops=1)
Index Cond: (a3 = 2)
-> Bitmap Index Scan on tmp_facts7_a2_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=371.275..371.275 rows=2221344 loops=1)
Index Cond: (a2 = 2)
Total runtime: 1251.841 ms
(11 rows)


SELECT relname, relpages, reltuples FROM pg_class
WHERE NOT relname LIKE 'pg_%' ORDER BY relpages DESC;
relname | relpages | reltuples
-----------------------------------+----------+-------------
tmp_facts7 | 216217 | 4.00001e+07
tmp_facts7_a4_idx | 87777 | 4.00001e+07
tmp_facts7_a3_idx | 87777 | 4.00001e+07
tmp_facts7_a2_idx | 87777 | 4.00001e+07
tmp_facts7_a1_idx | 87777 | 4.00001e+07


Нормализованная таблица со справочниками (40M записей + 10,100,1k,10k записей в справочниках)

select count(*)
from tmp_facts7
join tmp_facts7_a1 on tmp_facts7.a1=tmp_facts7_a1.id
join tmp_facts7_a2 on tmp_facts7.a2=tmp_facts7_a2.id
join tmp_facts7_a3 on tmp_facts7.a3=tmp_facts7_a3.id
join tmp_facts7_a4 on tmp_facts7.a4=tmp_facts7_a4.id
where tmp_facts7_a1.name='00' and tmp_facts7_a2.name='00' and tmp_facts7_a3.name='00' and tmp_facts7_a4.name='00';
count
-------
391
(1 row)

explain analyze
select count(*)
from tmp_facts7
join tmp_facts7_a1 on tmp_facts7.a1=tmp_facts7_a1.id
join tmp_facts7_a2 on tmp_facts7.a2=tmp_facts7_a2.id
join tmp_facts7_a3 on tmp_facts7.a3=tmp_facts7_a3.id
join tmp_facts7_a4 on tmp_facts7.a4=tmp_facts7_a4.id
where tmp_facts7_a1.name='00' and tmp_facts7_a2.name='00' and tmp_facts7_a3.name='00' and tmp_facts7_a4.name='00';
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=124783.94..124783.95 rows=1 width=0) (actual time=1267.201..1267.202 rows=1 loops=1)
-> Nested Loop (cost=104399.33..124783.94 rows=1 width=0) (actual time=1217.377..1267.021 rows=391 loops=1)
Join Filter: (tmp_facts7.a1 = tmp_facts7_a1.id)
-> Nested Loop (cost=104399.33..124782.80 rows=1 width=4) (actual time=1217.140..1234.743 rows=6768 loops=1)
-> Nested Loop (cost=0.00..19.81 rows=1 width=12) (actual time=0.070..0.096 rows=1 loops=1)
-> Nested Loop (cost=0.00..11.53 rows=1 width=8) (actual time=0.056..0.078 rows=1 loops=1)
-> Seq Scan on tmp_facts7_a2 (cost=0.00..3.25 rows=1 width=4) (actual time=0.010..0.029 rows=1 loops=1)
Filter: (name = '00'::text)
-> Index Scan using tmp_facts7_a4_name_idx on tmp_facts7_a4 (cost=0.00..8.27 rows=1 width=4) (actual time=0.044..0.046 rows=1 loops=1)
Index Cond: (tmp_facts7_a4.name = '00'::text)
-> Index Scan using tmp_facts7_a3_name_idx on tmp_facts7_a3 (cost=0.00..8.27 rows=1 width=4) (actual time=0.012..0.016 rows=1 loops=1)
Index Cond: (tmp_facts7_a3.name = '00'::text)
-> Bitmap Heap Scan on tmp_facts7 (cost=104399.33..124660.93 rows=5832 width=16) (actual time=1217.066..1229.133 rows=6768 loops=1)
Recheck Cond: ((tmp_facts7.a4 = tmp_facts7_a4.id) AND (tmp_facts7.a3 = tmp_facts7_a3.id) AND (tmp_facts7.a2 = tmp_facts7_a2.id))
-> BitmapAnd (cost=104399.33..104399.33 rows=5832 width=0) (actual time=1214.610..1214.610 rows=0 loops=1)
-> Bitmap Index Scan on tmp_facts7_a4_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=363.302..363.302 rows=2224826 loops=1)
Index Cond: (tmp_facts7.a4 = tmp_facts7_a4.id)
-> Bitmap Index Scan on tmp_facts7_a3_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=364.123..364.123 rows=2223561 loops=1)
Index Cond: (tmp_facts7.a3 = tmp_facts7_a3.id)
-> Bitmap Index Scan on tmp_facts7_a2_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=366.062..366.062 rows=2221344 loops=1)
Index Cond: (tmp_facts7.a2 = tmp_facts7_a2.id)
-> Seq Scan on tmp_facts7_a1 (cost=0.00..1.12 rows=1 width=4) (actual time=0.001..0.003 rows=1 loops=6768)
Filter: (tmp_facts7_a1.name = '00'::text)
Total runtime: 1268.177 ms
(24 rows)

select count(*)
from tmp_facts7
where
tmp_facts7.a1 IN (select id from tmp_facts7_a1 where name='00') and
tmp_facts7.a2 IN (select id from tmp_facts7_a2 where name='00') and
tmp_facts7.a3 IN (select id from tmp_facts7_a3 where name='00') and
tmp_facts7.a4 IN (select id from tmp_facts7_a4 where name='00');
count
-------
391
(1 row)

explain analyze
select count(*)
from tmp_facts7
where
tmp_facts7.a1 IN (select id from tmp_facts7_a1 where name='00') and
tmp_facts7.a2 IN (select id from tmp_facts7_a2 where name='00') and
tmp_facts7.a3 IN (select id from tmp_facts7_a3 where name='00') and
tmp_facts7.a4 IN (select id from tmp_facts7_a4 where name='00');
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=298421.25..298421.26 rows=1 width=0) (actual time=6530.685..6530.686 rows=1 loops=1)
-> Nested Loop IN Join (cost=34817.31..298421.24 rows=1 width=0) (actual time=493.943..6530.065 rows=391 loops=1)
Join Filter: (tmp_facts7.a2 = tmp_facts7_a2.id)
-> Hash IN Join (cost=34817.31..298417.98 rows=1 width=4) (actual time=471.609..6381.263 rows=6898 loops=1)
Hash Cond: (tmp_facts7.a1 = tmp_facts7_a1.id)
-> Hash IN Join (cost=34816.17..298416.82 rows=4 width=8) (actual time=471.453..6320.043 rows=124045 loops=1)
Hash Cond: (tmp_facts7.a3 = tmp_facts7_a3.id)
-> Nested Loop (cost=34807.89..298393.49 rows=4000 width=12) (actual time=471.244..5245.973 rows=2224826 loops=1)
-> HashAggregate (cost=8.28..8.29 rows=1 width=4) (actual time=0.066..0.068 rows=1 loops=1)
-> Index Scan using tmp_facts7_a4_name_idx on tmp_facts7_a4 (cost=0.00..8.27 rows=1 width=4) (actual time=0.061..0.062 rows=1 loops=1)
Index Cond: (name = '00'::text)
-> Bitmap Heap Scan on tmp_facts7 (cost=34799.61..272069.32 rows=2105271 width=16) (actual time=471.174..3375.398 rows=2224826 loops=1)
Recheck Cond: (tmp_facts7.a4 = tmp_facts7_a4.id)
-> Bitmap Index Scan on tmp_facts7_a4_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=370.219..370.219 rows=2224826 loops=1)
Index Cond: (tmp_facts7.a4 = tmp_facts7_a4.id)
-> Hash (cost=8.27..8.27 rows=1 width=4) (actual time=0.050..0.050 rows=1 loops=1)
-> Index Scan using tmp_facts7_a3_name_idx on tmp_facts7_a3 (cost=0.00..8.27 rows=1 width=4) (actual time=0.047..0.049 rows=1 loops=1)
Index Cond: (name = '00'::text)
-> Hash (cost=1.12..1.12 rows=1 width=4) (actual time=0.013..0.013 rows=1 loops=1)
-> Seq Scan on tmp_facts7_a1 (cost=0.00..1.12 rows=1 width=4) (actual time=0.009..0.011 rows=1 loops=1)
Filter: (name = '00'::text)
-> Seq Scan on tmp_facts7_a2 (cost=0.00..3.25 rows=1 width=4) (actual time=0.004..0.019 rows=1 loops=6898)
Filter: (tmp_facts7_a2.name = '00'::text)
Total runtime: 6531.779 ms
(24 rows)


select count(*)
from tmp_facts7,tmp_facts7_a1,tmp_facts7_a2,tmp_facts7_a3,tmp_facts7_a4
where tmp_facts7.a1=tmp_facts7_a1.id and tmp_facts7.a2=tmp_facts7_a2.id and tmp_facts7.a3=tmp_facts7_a3.id and tmp_facts7.a4=tmp_facts7_a4.id
and tmp_facts7_a1.name='00' and tmp_facts7_a2.name='00' and tmp_facts7_a3.name='00' and tmp_facts7_a4.name='00';
count
-------
391
(1 row)


explain analyze
select count(*)
from tmp_facts7,tmp_facts7_a1,tmp_facts7_a2,tmp_facts7_a3,tmp_facts7_a4
where tmp_facts7.a1=tmp_facts7_a1.id and tmp_facts7.a2=tmp_facts7_a2.id and tmp_facts7.a3=tmp_facts7_a3.id and tmp_facts7.a4=tmp_facts7_a4.id
and tmp_facts7_a1.name='00' and tmp_facts7_a2.name='00' and tmp_facts7_a3.name='00' and tmp_facts7_a4.name='00';

QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=124783.94..124783.95 rows=1 width=0) (actual time=1261.212..1261.213 rows=1 loops=1)
-> Nested Loop (cost=104399.33..124783.94 rows=1 width=0) (actual time=1211.897..1261.036 rows=391 loops=1)
Join Filter: (tmp_facts7.a1 = tmp_facts7_a1.id)
-> Nested Loop (cost=104399.33..124782.80 rows=1 width=4) (actual time=1211.666..1228.751 rows=6768 loops=1)
-> Nested Loop (cost=0.00..19.81 rows=1 width=12) (actual time=0.074..0.101 rows=1 loops=1)
-> Nested Loop (cost=0.00..11.53 rows=1 width=8) (actual time=0.061..0.082 rows=1 loops=1)
-> Seq Scan on tmp_facts7_a2 (cost=0.00..3.25 rows=1 width=4) (actual time=0.013..0.032 rows=1 loops=1)
Filter: (name = '00'::text)
-> Index Scan using tmp_facts7_a4_name_idx on tmp_facts7_a4 (cost=0.00..8.27 rows=1 width=4) (actual time=0.046..0.048 rows=1 loops=1)
Index Cond: (tmp_facts7_a4.name = '00'::text)
-> Index Scan using tmp_facts7_a3_name_idx on tmp_facts7_a3 (cost=0.00..8.27 rows=1 width=4) (actual time=0.011..0.015 rows=1 loops=1)
Index Cond: (tmp_facts7_a3.name = '00'::text)
-> Bitmap Heap Scan on tmp_facts7 (cost=104399.33..124660.93 rows=5832 width=16) (actual time=1211.587..1223.046 rows=6768 loops=1)
Recheck Cond: ((tmp_facts7.a4 = tmp_facts7_a4.id) AND (tmp_facts7.a3 = tmp_facts7_a3.id) AND (tmp_facts7.a2 = tmp_facts7_a2.id))
-> BitmapAnd (cost=104399.33..104399.33 rows=5832 width=0) (actual time=1209.131..1209.131 rows=0 loops=1)
-> Bitmap Index Scan on tmp_facts7_a4_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=362.778..362.778 rows=2224826 loops=1)
Index Cond: (tmp_facts7.a4 = tmp_facts7_a4.id)
-> Bitmap Index Scan on tmp_facts7_a3_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=362.364..362.364 rows=2223561 loops=1)
Index Cond: (tmp_facts7.a3 = tmp_facts7_a3.id)
-> Bitmap Index Scan on tmp_facts7_a2_idx (cost=0.00..34273.29 rows=2105271 width=0) (actual time=362.790..362.790 rows=2221344 loops=1)
Index Cond: (tmp_facts7.a2 = tmp_facts7_a2.id)
-> Seq Scan on tmp_facts7_a1 (cost=0.00..1.12 rows=1 width=4) (actual time=0.001..0.003 rows=1 loops=6768)
Filter: (tmp_facts7_a1.name = '00'::text)
Total runtime: 1262.187 ms
(24 rows)


Выводы

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

(C) Alexey Pechnikov aka MBG, mobigroup.ru