логин: 
<< предыдущая заметкаследующая заметка >>
28 декабря 2010
вопрос по MyQSL

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

Практика показала, что пользуются учетным номером в полной мере (регистрируются, прописывают логин-пароль, заполняют карточку) не более 1% посетителей. Глупо хранить пустую на 99% mysql-таблицу с кучей полей. Глупо искать в базе данные при каждом обращении, хотя в 99% случаев данных нет.

SELECT COUNT(*) FROM `unic`
651144

SELECT COUNT(*) FROM `unic` WHERE (`login`!='' AND `password`!='') OR `openid`!=''
4140

SELECT COUNT(*) FROM `unic` WHERE (`login`!='' AND `password`!='') OR `openid`!='' OR `realname`!='' OR `lju`!='' OR `mail`!='' OR `site`!='' OR `birth`!='0000-00-00'
42769

Но PHP не позволяет где-то хранить или генерировать новый номер+1 с гарантией отсутствия дублей. Как быть? Варианта я вижу два:

1. Две таблицы. В одной — только номер, для которого выполянется auto_increment, после чего номер можно выдать в куку посетителю, а запись смело удалять:

CREATE TABLE `uni` (
`id` int(10) unsigned NOT NULL auto_increment,
)

В другой — вся регистрационная информация, если посетитель ее ввел:

CREATE TABLE `reg` (
`id` int(10) unsigned NOT NULL auto_increment,
`uni` int(10) unsigned NOT NULL,
`login` .....
`password` .....
`realname` ....
....
)

2. Одна таблица `reg` (первую таблицу `uni` не делать). Вместо этого выдавать посетителю длинный номер BIGINT вида 2010122611530000001, где первые цифры 201012261153 — число и время обращения за номером, а 0000001 — случайное число.
Это не вполне чистое решение, потому что теоретически при обращении в одну и ту же секунду, скрипт может выдать два одинаковых номера. Кроме того, тогда придется всюду в учетных записях указывать номер посетителя bigint вместо int, не слишком ли это большая трата ресурсов?

<< предыдущая заметка следующая заметка >>
пожаловаться на эту публикацию администрации портала
архив понравившихся мне ссылок
Оставить комментарий
Linux Safari Chrome
2
0
почему бы не использовать UUID'ы?
http://en.wikipedia.org/wiki/Uuid
Linux Firefox
 Москва
0
0
Я не умею читать на иностранном языке.
Можно вкратце - что это?
Linux Firefox
 Москва
0
0
А, нашел на русском.
А в чем, простите, отличие UUID от любой другой кодовой строки? Тот же md5, только разделенный в четырех местах минусиками?
Linux Safari Chrome
0
0
Отличие в том, что это встроенный в язык способ сгенерировать уникальную строку, которая не будет никогда повторяться ни при каких условиях.

SELECT UUID();
Linux Firefox
 Москва
0
0
В какой язык? В PHP еще встроены md5 и sha1, например. Тоже генерируют уникальную неповторимую строку. И что?
Linux Safari Chrome
3
0
UUID это стандартизированный вариант решения задачи, которую вы сформулировали в варианте 2. Каждый UUID уникален и весит всего 16 байт. В вашем варианте "2010122611530000001" это 19 байт с относительно высокой вероятностью дупликатов.

Из русской вики:
"Общее количество уникальных ключей UUID составляет 2128 = 25616 или около 3.4 ? 1038. Это означает, что генерируя 1 триллион ключей каждую наносекунду, перебрать все возможные
значения удастся лишь за 10 миллиардов лет."

MD5 и SHA1 не решают вашей задачи, они не генерируют строку, они её модифицируют. Грубо говоря, им на вход нужно давать что-то. И если это что-то генерировать рандомом, это получится просто тоже самое, но запутанней.

Т. к. UUID - стандарт, уже существует множество готовых библиотек под любые языки программирования. Думаю что под PHP тоже существует какая-то стандартная библиотека (я не работаю с этим языком, чтобы сказать уверенней), но быстрый поиск дал вариант проще, вот функция которая сгененрирует вам UUID: http://codepaste.net/ofwuzz
Linux Firefox
 Москва
0
0
Я использую идентификатор пользователя в виде номера.

Помимо номера в куках некий md5 с солью и перцем, который подтверждает, что предъявляемый номер действительно выдан мной, а не сфальсифицирован пользователем.

Сам по себе абстрактный рандомный UUID для этой задачи вообще не годится. Что я с ним буду делать? Я выдам пользователю один UUID, а он подставит свой UUID, и я даже не узнаю об этом. Или предлагаете мне бережно хранить на своей стороне все UUID, которые я когда-либо выдавал пользователям, и по предъявлении делать поиск по гигантской базе среди индексированного поля varchar(64)? Так это будет не улучшение существующей сейчас системы с оптимизацией ресурсов, а многократное ее ухудшение.
Linux Safari Chrome
1
0
Мне кажется, вы не так поняли что такое UUID. Это по большому счёту и есть 128-битное (16-байтное) случайное число. Точнее стандарт генерации этого числа - не все биты там случайные. Просто если его записать в 16-ричном виде, получается 40 текстовых символов, наверное это вас и смутило. Но это просто форма представления.

Та функция, которую я скинул в прошлом сообщении, генерирует случайный UUID в текстовом виде. В таком виде вы его кладёте в кукизы и выводите в админке (причина того что ююиды разделены дефизами - именно для улучшения восприятия). С помощью этой строчки, сгенерированный текстовый UUID можно превратить в число:

$bin = pack("h*", str_replace('-', '', $uuid));

А после этого сохранить его в БД, в поле BINARY(16).

UUID и надо использовать как "идентификатор пользователя в виде номера", тут вы совершенно правы, именно для таких задач он и придуман. В распределённых базах данных, ююиды вообще используются как PRIMARY KEY в таблицах, вместо автоинкремента.

Ещё сделаю небольшую ремарку. Я программирую на Python, и поэтому, когда я узнал о существовании ююидов, передо мной не стоял вопрос использовать их или не использовать. Т. к. в питоне эта штука уже встроена, мне было гораздо удобнее вызвать одну готовую функцию, чем писать что-то своё подобное. Но если в вашем случае, в PHP нет стандартизированных инструментов генерации уникальных идентификаторов, возможно вам и не стоит заморачиваться, а действительно, просто самому сгенерировать случайное число, выдать пользователю и записать в базу. В конце концов, единственное, зачем нужно следовать стандартам - чтобы те люди, которые будут позже разбираться в вашем коде не разбирались в лишних придуманных сущностях. Если это не входит в ваши приоритеты, сделайте как проще. Без иронии, действительно так считаю.
Linux Firefox
 Москва
0
4
Странный у нас получается разговор, не находите? Примерно так:

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

Вы: Есть прекрасное решение: хранить на каждого посетителя не один лист, а целую папку со случайными кодами!

Я: Кажется вы не поняли, мне даже листки с номером мешают, зачем мне папка с кодами?

Вы: Вы не понимаете! Это чудесная папка с волшебным случайным кодом на 100 страницах! Этот код уникален! Другого такого в мире не найти! Его гениальность в том, что он придуман фирмой Microsoft и вызывается прекрасной встроенной функцией! Вы обязательно должны завести такую папку на каждого своего посетителя!

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

Вы: Нет-нет! Вы не поняли, о чем я говорю! Вы не хотите понять! Это чудесная неповторимая папка с кодом! Она волшебная! И толщиной всего 2 сантиметра! Вам надо завести такую папку и хранить ее на каждого посетителя! Причем, в каждом из ваших шкафов, где вы ведете работу с посетителями!

И так далее по кругу.
Linux Safari Chrome
2
0
Простите, но я не нахожу, что это так.

> Вы: Есть прекрасное решение: хранить на каждого посетителя не один лист, а целую папку со случайными кодами!

Вот это то место где вы меня неправильно поняли. Я не предлагаю заводить на каждого посетителся какую-то папку. Как раз я согласен с вашим вариантом - каждому посетителю один номер. Просто этот номер не с балды генерить, а использовать в качестве такого номера UUID. "UUID" расшифровывается как "Универсальный Уникальный Идентификатор", т. е. это та штука которую все используют в качестве уникальных номеров любым объектам, в вашем случае - пользователям. Этот стандарт придумала Open Software Foundation, а не Microsoft. Microsoft, по большому счёту - конкурент OSF, они, позже, сделали реализацию этого стандарта и назвали её "GUID", но это не имеет отношения к делу.

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

Опять же, мне кажется, что в вашем случае, возможно не имеет смысла разбираться с ненужными стандартами, если это не входит в ваши приоритеты.
Linux Firefox
 Москва
0
0
Боже... Попробую еще раз описать, как минимум, две проблемы:

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

2. И в базе пользователей, и во всех прочих базах (голосования, комментарии, плюсики-минусики, журнал посещений и т.п.), где я использовал ключевое поле INT, мне придется иcпользовать ключевое поле varchar(16). Что, мягко говоря грубо выражаясь, очень даже совсем не улучшит быстродействие и очень даже не сократит объемы данных в таблицах.
Windows Firefox
1
0
quantum (#644921)
Загребает пытался объяснить, что вместо "BIGINT вида 2010122611530000001" можно использовать uuid, чтобы не было "теоретически при обращении в одну и ту же секунду, скрипт может выдать два одинаковых номера".

"номер посетителя bigint вместо int" и даже BINARY(16), как предлагали, не будет большой тратой ресурсов, учитывая, что вы хотите избавиться от 651144 пустых записей и оставить только 4140+42769 заполненных
эти хэш функции не гарантируют уникальности.
Хотя вероятность коллизии в данном случае маловероятна
Mac Safari
0
0
armyakov
1. Инкремент, чтение и удаление не даст гарантии атомарности. Лучше использовать из MySQL 5.0 функцию last_insert_id()

Create a table to hold the sequence counter and initialize it:

mysql> CREATE TABLE sequence (id INT NOT NULL);
mysql> INSERT INTO sequence VALUES (0);
Use the table to generate sequence numbers like this:

mysql> UPDATE sequence SET id=LAST_INSERT_ID(id+1);
mysql> SELECT LAST_INSERT_ID();
The UPDATE statement increments the sequence counter and causes the next call to LAST_INSERT_ID() to return the updated value. The SELECT statement retrieves that value. The mysql_insert_id() C API function can also be used to get the value. See Section 20.8.3.37, “mysql_insert_id()”.

Функция возвращает значение для текущего соединения с базой, а update гарантирует атомарность.
Можно завести таблицу для сиквенсов, добавлять в нее поля с именем последовательности и сделать функци на пхп в которую передается название, а она увеличивает сиэквенс и возвращает значение.
seq_getnext(seq_name)
UPDATE sequence SET seq_name=LAST_INSERT_ID(seq_name+1);
SELECT LAST_INSERT_ID();

2. microtime() + сборка результата в bigint , два обращения в микросекунду маловероятны.
Linux Firefox
 Москва
0
0
А почему тогда просто не сделать "учетное" поле в некой таблице и увеличивать его?

UPDATE `site` SET `value`=`value`+1 WHERE `name`='last_id'
SELECT `value` FROM `site` WHERE `name`='last_id'
Windows Firefox
0
0
armyakov
не гарантирует атомарность, если два запроса будут выполняться одновременно.

последовательно
a=5
1.update set a=a+1;
1.select a;
2.update set a=a+1;
2.select a;

result.1=6
result.2=7
OK
===
параллельно
a=5
1.UPDATE SET a=a+1;
2.UPDATE SET a=a+1;
1.SELECT a;
2.SELECT a;

result.1=7
result.2=7
не ОК

LAST_INSERT_ID() возвращает последнее записанное значение в текущем соединении с базой
т.е. если есть десяток подключений, и в каждом идет UPDATE, то каждому вернется значение, записанное именно в его соединении с базой, а не последний UPDATE на текущий момент.

В этом случае между update и select можно хоть час поставить паузу, главное не выполнять других запросов к базе и не рвать соединение.
Nokia-E90 Safari
 Москва
0
0
Leonid Kaganov
теперь ясно
Windows Firefox
0
0
armyakov
1. ручная реализация sequence для MySQL, почему их до сих пор нет в этой базе -- загадка.
Для задач, когда нужен независимый счетчик для вообще всех объектов, уникальный и увеличивающийся.
Linux Firefox
 Москва
0
0
Иными словами: оптимальный вариант - оставить все как есть, но только по крону (или по новому комментарию) удалять пустые поля.

if(msq_add("INSERT ...."))===false) { trevoga("DB ADD FALSE!!!!"); }
$unic=mysql_insert_id(); if(!$unic) { trevoga("insert_id():".$unic); }

Шо интересно: за весь год "trevoga()" в логе ни разу не сработала, хотя я поначалу опасался каких-то сбоев. Надо ее убирать.
Windows Safari Chrome
0
0
armyakov
По возможности DELETE следует избегать.
Это очень дорогая операция, тем более на таблицах с миллионами записей.
Windows Safari Chrome
0
0
armyakov
Есть исчезающе малая вероятность появления двух одинаковых номеров.


mysql> create table sequences (guid int(10) unsigned not null auto_increment, primary key (guid));
Query OK, 0 rows affected (0.04 sec)

mysql> insert into sequences values(NULL);
Query OK, 1 row affected (0.02 sec)

mysql> select * from sequences;
+------+
| guid |
+------+
| 1 |
+------+
1 row in set (0.00 sec)

mysql> insert into sequences values(NULL);
Query OK, 1 row affected (0.01 sec)

mysql> insert into sequences values(NULL);
Query OK, 1 row affected (0.00 sec)

mysql> insert into sequences values(NULL);
Query OK, 1 row affected (0.00 sec)

mysql> select * from sequences;
+------+
| guid |
+------+
| 1 |
| 2 |
| 3 |
| 4 |
+------+
4 rows in set (0.00 sec)

mysql> delete from sequences;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into sequences values(NULL);
Query OK, 1 row affected (0.00 sec)

mysql> select * from sequences;
+------+
| guid |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
Windows Firefox
3
0
get_lock()
set @uni:[email protected]+1;
release_lock()

ну и при старте сервака
select max() into @uni from ...
Linux Ubuntu Firefox
0
0
О, первый дельный коммент в этом посте!
Linux Firefox
 Москва
0
0
Только я не понял, на каком он языке...
Linux Ubuntu Firefox
0
0
Очевидно же, что на языке хранимых процедур MySQL.
Но решение универсально, можно написать и на PHP.
Windows Firefox
0
0
armyakov
это локальная переменная на время работы процесса
Windows Firefox
0
0
Это _пример_ :)
Переменная может быть и пхп-шной, если они там есть, и вообще чем угодно глобальным.
Windows Safari Chrome
0
0
А почему глупо хранить разряженную MYSQL-таблицу? Я бы сделал именно так - таблицу, совмещающую в себе uni и reg.
Пустые колонки места на диске физически не занимают, поэтому переживать, что каждый новый посетитель создает новую запись в БД, не стоит. Разумно только не создавать записи для заведомо единичных посещений (например, поисковые роботы), и ограничить время хранения незарегистрированных записей, если они повторно не заходят (завести колонку "время последнего обращения", обновляющуюся при каждом запросе браузера, а по крону периодически удалять незарегистрированные записи, время последнего обращения у которых больше года, скажем).
При таком подходе любое первичное обращение пользователя, по существу, считается его регистрацией на сайте (подтверждение по почте не обязательно). На пользователя заводится (пустая) карточка, которую, при желании, он может заполнить (опять же, частично). Например, я не хочу разглашать свой емэйл, но желаю указать дату своего рождения - пожалуйста!
В любой момент пользователь может подтвердить свою регистрацию, указав емэйл или OpenID
Linux Firefox
 Москва
0
0
Так сейчас и хранится. Но таблица на 99% пустая. В ней чуть меньше 1000000 записей, из них зарегистрированных - меньше 4000. Размер 30мб. Это один мой дневник за год. А если будет сервис на 1000 дневников за три года? И при каждом заходе нового посетителя делать поиск по этой колоссальной базе?
Windows Safari Chrome
0
0
1) Подозреваю, что из этой кучи записей еще процентов девяносто - однократные или двукратные случайные заходы (кстати, ты роботов откидываешь?). По прошествии определенного времени их можно безболезненно грохнуть одним SQL-запросом. Этим "сроком давности" можно регулировать размер базы: стала база сильно большая - сокращаем срок хранения записи.
2) И что, какая сложность в поиске по первичному ключу? Да хоть миллиард записей будет, это сильно не напряжет сервер.
3) Тридцать мегов база на миллион записей - это порядка тридцати байтов на одну запись. Что-то мне кажется, что многовато для почти пустой строки. Какая структура у таблицы посетителей?
Nokia-E90 Safari
 Москва
0
0
Leonid Kaganov
По прошествии времени я ничего не грохаю - а вдруг посетитель зайдет через три года снова? Ненавижу этот идиотизм, когда твою регистрацию грохают, как только ты отвернулся или отвлекся.
Регистрация - это когда пользователь хоть что-то о себе написал, а если он сделал один запрос (скажем, на сайт символического направления :) ), ничего про себя не писал, и больше его уникальная кука не всплывала в течении года, то нахер хранить такую "регистрацию"
Nokia-E90 Safari
 Москва
0
0
Leonid Kaganov
Еще вариант: отметился в журнале посещений. А это значит, каждый пользователь.
Windows Firefox
0
0
Andrey_aka_Mem0 (loki-gh0st)
>если зайдёт через 3 года

Но у него ведь не было регистрации, всех дел, у него будет уже другой браузер и другой компьютер. Зачем хранить запись незарегистрированного пользователя бесконечно? чтобы узнать когда он зайдёт в следующий раз? некоторые меняют ОС и айпи каждый день.
Windows Firefox
0
0
lleo.me/[email protected]Артем Павлов
Мне кажется, либо оставить так, как есть, а старые пустые записи удалять по крону, либо воспользоваться вторым вариантом. Тогда сделать первую часть кода датой-временем первого захода, а вторую чем-то хешеобразным, сгенерированым на основании какой-то информации, которую передал браузер юзера. Это должно минимизировать риск появления одинаковых идешников до бесконечно малого.
Windows Firefox
0
0
lleo.me/[email protected]Артем Павлов
Есчо. По поводу подтверждения мыла для незарегистрированных можно не париться. Тут регистрация с точки зрения пользователя сделана невероятно просто - даже страницу перезагружать не надо! Но если человек не хочет потратить десять секунд на то, чтобы личную карточку заполнить, то и мейл он подтверждать не станет, это ж, сцуко, еще в почтовый ящик заходить надо!
Linux Firefox
 Москва
0
0
Наверно имеет смысл так и делать. По крайней мере, сильно технологичнее с точки зрения переписывания существующего кода.

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

А вот с md5 хотелось бы разобраться. Говорят, он взламывается перебором за несколько часов при помощи софтин, использующих процессоры видеокарт. Надо спрашивать Зефирова.
Windows Firefox
0
0
lleo.me/[email protected]Артем Павлов
Ну да, про грядущую многопользовательность я и забыл...

Про md5 не понял. Я говорю про идентификатор пользователя, который и так виден, стоит линую карточку открыть. А так, да, взламывается. Штука в том, что одному md5-хешу могут соответствовать несколько разных текстовых строк.
Windows Opera
0
1
Gryzchick (#462716)
По-моему, наиболее удобным способом оптимизации будет установка NoSQL-СУБД, или перевод в подобный режим MySQL.
Ты это серьёзно сказал или ты всё-таки читал исходный пост Ллео?
Mac Safari
1
0
Павел (#441666)
О, это ведь старая задача генерации уникальных msgid в фидо. :)

Я бы сделал две таблицы, как в первом варианте (и смотреть номер через last_insert_id(), как правильно сказали, никаких microtime()), но в первую таблицу добавил бы второй столбец - индекс в таблице 'reg'. Чтобы поиском по первой таблице можно было сразу понять, регистрировался человек или нет, надо ли искать его данные во второй таблице.

Другой вариант: в одной таблице просто счётчик (один столбец, одна строка, атомарное увеличение), в другой - регистрационные данные, а хранить информацию о том, зарегистрирован ли пользователь, на стороне пользователя, и не искать его данные в таблице 'reg', если он сам куками не сказал, что он зарегистрирован.

Применение алгоритмов, которые изначально (by design) с некоторой (малой) вероятностью дают глюк, я считаю злом. Глюков обычно хватает и от реализации, не нужно их вероятность изначально в алгоритм закладывать, даже если эта вероятность мала.
Windows Firefox
0
0
3:712/550 (#563689)
Собственно, можно сделать отдельно таблицу-счетчик с одной записью-счетчиком, который наращивать при создании нового пользователя. А существующую таблицу unic оставить как есть, но записывать туда данные только когда кто-то регистрируется.

Если делать уникальные ID без счетчика (как в пункте 2, только придумать алгоритм чтобы не было одинаковых номеров 100%, или, как уже предлагали, через GUID) - то оставить все как есть, просто не записывать данные пустые.
Windows Firefox
0
0
Иван (#465206)
Встаёт вопрос о проектировании базы. Это очень похоже на вопрос о "нормализации" базы данных. В большинстве учебников по СУБД этот вопрос рассмотрен весьма разносторонне (если не лень читать многобукв, то я бы советовал кнгижку, у коей автор Дейт).
Подозреваю, что вопрос решается просто разделением таблицы на две. В принципе если б я знал структуру базы, то просто мог бы что называется ткнуть пальшцем где и чего надо подпровлять.
Linux Firefox
 Москва
0
0
Поражает здесь другое: люди, предлагающие все бросить и идти читать какие-то книжки и учебники, сами не в состоянии внимательно прочесть несчастные два абзаца формулировки задачи.

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

всего комментариев: 49

<< предыдущая заметка следующая заметка >>