Пользователи на сайте: Memcached vs MySQL-JOIN

Здравствуйте, дорогие друзья, с вами снова Блог ГО.

Сегодня, в День Знаний, разберём одну тему. Тема, не так, чтобы для старших классов, но уже и не для дошкольников.

Рассмотрим один из вариантов хранения данных о пользователях в мемкэше.

Постановка задачи

Есть, допустим, сайт, а на нём есть зарегистрированные пользователи. Их данные хранятся, обычно, в таблице наподобие следующей:

CREATE TABLE `users` (
	`user_id`  INT UNSIGNED NOT NULL AUTO_INCREMENT,
	`username` VARCHAR(20)  NOT NULL,              -- Имя
	`surname`  VARCHAR(20)  NOT NULL,              -- Фамилия
	`nickname` VARCHAR(50)  NULL DEFAULT NULL,     -- Ник
	`avatar`   BOOL         NOT NULL DEFAULT "0",  -- Наличие аватара
	`status`   ENUM("active", "banned", "deleted") DEFAULT "active", -- Активный, забаненый, удалённый
	-- ...
	PRIMARY KEY (`user_id`)
	-- ...
);


И есть различные ресурсы с которыми пользователи связаны. Например, сообщения на форуме:

CREATE TABLE `posts` (
	`post_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
	`topic`   INT UNSIGNED NOT NULL,           -- в какой теме написано
	`author`  INT UNSIGNED NOT NULL,           -- FOREIGN `users`.`user_id`
	`posted`  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,  -- время сообщения
	`message` TEXT NOT NULL,                   -- текст сообщения
	-- ...
	PRIMARY KEY (`post_id`),
	KEY (`topic`),
	KEY (`author`)
	-- ...
);

Ну и стандартная задача — вывести сообщения из заданной темы, вместе с их авторами (ФИО автора и аватар сбоку).

Частое решение (3-я страница 77-й темы):

SELECT `p`.`post_id`, `p`.`posted`, `p`.`message`, `u`.`user_id`, `u`.`username`, `u`.`surname`, `u`.`nickname`, `u`.`avatar`
	FROM `posts` AS `p`
	LEFT JOIN `users` AS `u` ON `u`.`user_id`=`p`.`author`
	WHERE `topic`=77
	ORDER BY `posted` ASC
	LIMIT 20, 10

И полученный результат:

array
  0 => 
    array
      'post_id' => string '1060' (length=4)
      'posted' => string '2010-08-29 19:33:10' (length=19)
      'message' => string 'Собственно, сабж' (length=30)
      'user_id' => string '33922' (length=5)
      'username' => string 'Станислав' (length=18)
      'surname' => string 'Беляев' (length=12)
      'nickname' => null
      'avatar' => string '0' (length=1)
  1 => 
    array
      'post_id' => string '1248' (length=4)
      'posted' => string '2010-08-29 19:33:10' (length=19)
      'message' => string '+1' (length=2)
      'user_id' => string '34093' (length=5)
      'username' => string 'Артемий' (length=14)
      'surname' => string 'Комаров' (length=14)
      'nickname' => null
      'avatar' => string '1' (length=1)
  2 => 
    array
      'post_id' => string '1366' (length=4)
      'posted' => string '2010-08-29 19:33:10' (length=19)
      'message' => string 'На пидистале!' (length=24)
      'user_id' => string '34465' (length=5)
      'username' => string 'Александр' (length=18)
      'surname' => string 'Семёнов' (length=14)
      'nickname' => null
      'avatar' => string '1' (length=1)
  ...

Постановка проблемы

Проблема в JOIN’е. При всех его преимуществах, с увеличением нагрузки начинают проявляться и недостатки:

  1. Тормознутый он.
  2. При параллельных запросах ещё более тормознутый.
  3. Если хотим использовать NDB Cluster — он ещё намного более тормознутый.
  4. Если хотим использовать шардинг — он вообще неприемлим.
  5. И вне зависимости от производительности, он не лучшим образом влияет на такие абстрактные вещи, как модульность, инкапсуляция, повторное использование кода и т.д.
    Мы в модели форума жойним таблицу из модели пользователей. И жойним её из множества других моделей. Со всем вытекающим бардаком.

Решение проблемы

Капитан Очевидность советует: не использовать JOIN.

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

Но ведь выбирать этих пользователей нам нужно чётко по их ID, а здесь вполне подойдёт любое key-value хранилище. Например, всеми нами любимый Memcached.

Т.е. просто кэшируем данные пользователей в мемкэше.

Что хранить в Memcached?

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

А нужно нам для этих списков немного — имя, фамилия, аватар и, возможно, состояние (забанен/нет).
Вот только эти данные и будем сохранять.

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

Получаем параметры пользователя

Диаграмка

Всё просто. Исходные данные лежат в БД, в таблице users. Нужные параметры мы кэшируем в мемкэше в переменных с ключами вида uname:$userId. И, на случай, если в одном сценарии один пользователь потребуется несколько раз, дополнительно кэшируем в локальном массиве.

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

Ну и напишем простенький классик для работы с этим всем.

class UserNames {
 
    /**
     * Шаблон ключа для мемкэша
     * @const string
     */
	const USER_CACHE_KEY = 'uname:{userId}';
 
    /**
     * Поля из таблицы пользователей, относящиеся к ФИО
     * @const string
     */
    const USER_FIELDS = '`user_id`,`username`,`surname`,`nickname`,`avatar`,`status`';
 
	/**
	 * Получить параметры ФИО пользователя
	 *
	 * @param int $userId
     *        ID пользователя
	 * @return array
     *         параметры или false, если такого пользователя нет
	 */
	public static function getParams($userId) {
        if (!isset(self::$localCache[$userId])) {
            self::$localCache[$userId] = self::getParamsFromCache($userId);
        }
        return self::$localCache[$userId];
	}
 
    /**
     * Получить параметры пользователя из кэша
     * @param int $userId
     *        ID пользователя
     * @return array
     *         параметы или false, если такого пользователя нет
     */
    private static function getParamsFromCache($userId) {
        $key   = self::getKeyCache($userId);
        $value = self::$cache->get($key);
        if (!$value) {
            $value = self::getParamsFromDB($userId);
            $value = self::toCache($value);
            self::$cache->set($key, $value);
        }
        return self::fromCache($value);;
    }
 
    /**
     * Получить параметры пользователя из БД
     * @param int $userId
     *        ID пользователя
     * @return array
     *         параметры или false, если такого пользователя нет
     */
    private static function getParamsFromDB($userId) {
        return
            self::$db->query(
                'SELECT ?q FROM `users` WHERE `user_id`=?i',
                array(self::USER_FIELDS, $userId),
                'rowassoc'
            );
    }
 
    /**
     * Преобразование параметров для сохранения в кэше
     *
     * @param array $params
     *        параметры полученные из базы
     * @return array
     *         данные для сохранения в кэше
     */
    private static function toCache($params) {
        if (!$params) {
            return 'no';
        }    
        return $params;
    }
 
    /**
     * Преобразование параметров полученных из кэша
     *
     * @param array $params
     *        данные полученные из кэша
     * @return array
     *         параметры для дальнейшего использования
     */
    private static function fromCache($params) {
        if (!is_array($params)) {
            return false;
        }        
        return $params;
    }
 
    /**
     * Получить имя ключа в кэше связанного с пользователем
     * 
     * @param int $userId
     *        ID пользователя
     * @return string
     *         имя ключа с его параметрами
     */
	private static function getKeyCache($userId) {
		return str_replace('{userId}', $userId, self::USER_CACHE_KEY);
	}
 
    /**
     * Локальный кэш
     * @var array
     */
	private static $localCache = array();
 
    /**
     * Объект для связи с базой данных
     * @var goDB
     */
	private static $db;
 
    /**
     * Объект для связи с мемкэшем
     * @var Memcached
     */
	private static $cache;
}

По коду:

  1. Для работы с мемкэшем используется стандартный php_memcached.
  2. Для работы с MySQL используется моя goDB, чтобы не забивать код примера разбором результата. Надеюсь, не знакомые с библиотекой люди всё разберутся в запросах, там всё просто.
  3. Для простоты примера всё оформлено как набор методов в статическом классе.
  4. По той же причине затронуто только получение данных, а не их запись и изменение.

Итак, пока у нас один публичный метод: getParams.

$userId = 10;
$user = UserNames::getParams($userId);
var_dump($user);
array
  'user_id'  => string '10' (length=2)
  'username' => string 'Антон' (length=10)
  'surname'  => string 'Герасимов' (length=18)
  'nickname' => null
  'avatar'   => string '0' (length=1)
  'status'   => string 'active' (length=6)

Для несуществующего пользователя возвращается FALSE.

Дабы не спутать хранимое FALSE от отсутствия ключа, в мемкэше хранится строка «no».
Преобразованием в него и обратно занимаются методы toCache и fromCache. Чуть позже они займутся у нас более интересными делами.

Получаем список пользователей

Тянуть по одному пользователю научились, но для списка это не кошерно.
Для него в Memcache есть мульти-гет, им и воспользуемся. Дополним наш класс.

class UserNames {
 
	// ...
 
    /**
     * Получить параметры для списка пользователей
     * 
     * @param array $uids
     *        список ID пользователей
     * @return array
     *         ассоциативный массив id=>параметры
     */
    public static function getParamsArray(array $uids) {
        $result = array();
        $nfound = array();
        foreach ($uids as $userId) {
            if (isset(self::$localCache[$userId])) {
                $result[$userId] = self::$localCache[$userId];
            } else {
                $nfound[] = $userId;
            }
        }
        if (count($nfound) > 0) {
            $fcache = self::getParamsArrayFromCache($nfound);
            $result += $fcache;
            self::$localCache += $fcache;
        }
        return $result;
    }
 
    /**
     * Получить параметры списка пользователей из кэша
     * 
     * @param array $uids
     *        список ID пользователей
     * @return array
     *         ассоциативный массив id=>параметры
     */
    private static function getParamsArrayFromCache($uids) {
        $keys = array();
        foreach ($uids as $userId) {
            $keys[] = self::getKeyCache($userId);
        }
        $users  = self::$cache->getMulti($keys);   
        foreach ($users as &$user) {
            $user = self::fromCache($user);
        }        
        $result = array();        
        foreach ($users as $key => $params) {
        	if (is_array($params)) {
            	$result[$params['user_id']] = $params;
            } else {
            	// hack
            	$u = explode(':', $key);
            	$result[$u[1]] = false;
            }
        }
        if ((count($result) < count($uids))) {
            $nfound = array();
            foreach ($uids as $userId) {
                if (!isset($result)) {
                    $nfound[] = $userId;
                }
            }
            $fdb = self::getParamsArrayFromDB($uids);
            $set = array();
            foreach ($fdb as $userId => $params) {
                $data = self::toCache($params);
                $key  = self::getKeyCache($userId);
                $set[$key] = $data;
                $result[$userId] = self::fromCache($data);
            }
            self::$cache->setMulti($set);
        }
        return $result;
    }
 
 
    /**
     * Получить параметры списка пользователей из БД
     *
     * @param array $uids
     *        список ID пользователей
     * @return array
     *         ассоциативный массив id=>параметры
     */
    private static function getParamsArrayFromDB($uids) {
        $res = self::$db->query(
                   'SELECT ?q FROM `users` WHERE `user_id` IN (?a)',
                   array(self::USER_FIELDS, $uids),
                   'assoc'
               );
        $result = array();
        foreach ($res as $r) {
            $result[$r['user_id']] = $r;
        }
        $uids = array_diff($uids, array_keys($result));
        if (count($uids) > 0) {
            foreach ($uids as $userId) {
                $result[$userId] = false;
            }
        }
        return $result;
    }        
 
}

Итак, у нас добавился публичный метод getParamsArray (я не парился с придумыванием названий), он получает список ID’шников и возвращает параметры пользователей:

$uids  = array(5, 7777777, 55);
$users = UserNames::getParamsArray($uids);
var_dump($users);
array
  5 => 
    array
      'user_id' => string '5' (length=1)
      'username' => string 'Дмитрий' (length=14)
      'surname' => string 'Григорьев' (length=18)
      'nickname' => null
      'avatar' => string '1' (length=1)
      'status' => string 'active' (length=6)
  7777777 => boolean false
  55 => 
    array
      'user_id' => string '55' (length=2)
      'username' => string 'Иван' (length=8)
      'surname' => string 'Борисов' (length=14)
      'nickname' => null
      'avatar' => string '0' (length=1)
      'status' => string 'active' (length=6)

Алгоритм прост:

1. Ищем запрошенных пользователей в локальном кэше, если каких-то не нашли, то только для них запрашиваем мемкэш (getParamsArrayFromCache).
2. В мемкэше делаем запрос мульти-выборку и сверяем список пришедших с запрошенными. Если каких-то не хватает, запрашиваем БД (getParamsArrayFromDB). Не забываем на этом этапе про преобразование toCache(), fromCache().
3. Из БД выбираем запросом: SELECT ... FROM `users` WHERE `user_id` IN ([список незакэшированных пользователей])

Дополнение массива

Ну и для упрощения вычислений в прикладном коде добавим ещё метод:

class UserNames {
 
	// ...
 
    /**
     * Заполнить массив параметрами пользователей
     *
     * @param array $A
     *        исходный массив
     * @param string $field
     *        поле, содержащее userId
     * @param string $res
     *        поле, куда записываются параметры
     * @return array
     *         итоговый массив
     */
    public static function fillArray(array $A, $field = 'user', $res = 'u') {
        $uids = array();
        foreach ($A as $params) {
            $uids[] = $params[$field];
        }
        $uids   = array_unique($uids);
        $result = self::getParamsArray($uids);
        foreach ($A as &$params) {
            $params[$res] = $result[$params[$field]];
        }
        return $A;
    }	
 
}

И для примера добудем снова теже сообщения с форума (уже без JOIN).

SELECT `post_id`, `posted`, `message`, `author` 
	FROM `posts` 
	WHERE `topic`=78 
	ORDER BY `posted` ASC 
	LIMIT 20,10

Результаты примерно такие:

array
  0 => 
    array
      'post_id' => string '1060' (length=4)
      'posted' => string '2010-08-29 19:33:10' (length=19)
      'message' => string 'Собственно, сабж' (length=30)
      'author' => string '33922' (length=5)
  1 => 
    array
      'post_id' => string '1248' (length=4)
      'posted' => string '2010-08-29 19:33:10' (length=19)
      'message' => string '+1' (length=2)
      'author' => string '34093' (length=5)
  ...

А теперь заполним этот массив пользователями.

$posts = $db->query('SELECT ... FROM `posts` ...');
$posts = UserNames::fillArray($posts, 'author');
var_dump($posts);

В $posts изначально описанный выше массив. "author" — поле указывающее на пользователя. Итог:

  0 => 
    array
      'post_id' => string '1060' (length=4)
      'posted' => string '2010-08-29 19:33:10' (length=19)
      'message' => string 'Собственно, сабж' (length=30)
      'author' => string '33922' (length=5)
      'u' => 
        array
          'user_id' => string '33922' (length=5)
          'username' => string 'Станислав' (length=18)
          'surname' => string 'Беляев' (length=12)
          'nickname' => null
          'avatar' => string '0' (length=1)
          'status' => string 'active' (length=6)
  1 => 
    array
      'post_id' => string '1248' (length=4)
      'posted' => string '2010-08-29 19:33:10' (length=19)
      'message' => string '+1' (length=2)
      'author' => string '34093' (length=5)
      'u' => 
        array
          'user_id' => string '34093' (length=5)
          'username' => string 'Артемий' (length=14)
          'surname' => string 'Комаров' (length=14)
          'nickname' => null
          'avatar' => string '1' (length=1)
          'status' => string 'active' (length=6)
  ...

В каждой записе о сообщении появилось поле 'u' с параметрами автора сообщения. При выводе их и используем.

Преобразования toCache и fromCache

Напомню, toCache() преобразует полученные из базы данные для сохранения в кэше, а fromCache() преобразует данные из кэша для их выдачи дальше.

Как их можно использовать?

Каждый раз при выводе мы склеиваем `username`,`surname`,`nickname` в ФИО и экранируем HTML-сущности. Это можно не делать каждый раз, а хранить уже сформированную строку в кэше.

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

Так же для забаненых и удалённых, например, нужно выводить не их настоящее имя, а что-то вроде «User Banned», «User Deleted». Храним в мемкэше сразу уже эти строки.

    private static function toCache($params) {
        if (!$params) {
            return 'no';
        }
        switch ($params['status']) {
        	case 'active':
        		$name   = $params['username'].' '.($params['nickname'] ? $params['nickname'].' ' : '').$params['surname'];
        		$name   = htmlspecialchars($name);
        		$avatar = $params['avatar'];
        		break;
        	case 'banned':
        		$name   = 'User Banned';
        		$avatar = 0;
        		break;
        	case 'deleted':
	        	$name   = 'User Deleted';
	        	$avatar = 0;
        		break;
        	default:
        		$name   = 'WTF?';
        		$avatar = 0;
        }   
        return $params['user_id'].'|'.$avatar.'|'.$name;
    }
 
    private static function fromCache($params) {
        $params = explode('|', $params, 3);
        if (count($params) != 3) {
        	return false;
        }
        return array(
        	'user_id' => (int)$params[0],
        	'avatar'  => (bool)$params[1],
        	'name'    => $params[2],
        );
    }

И в итоге получаем более компактную версию:

array
  0 => 
    array
      'post_id' => string '1060' (length=4)
      'posted' => string '2010-08-29 19:33:10' (length=19)
      'message' => string 'Собственно, сабж' (length=30)
      'author' => string '33922' (length=5)
      'u' => 
        array
          'user_id' => int 33922
          'avatar' => boolean false
          'name' => string 'Станислав Беляев' (length=31)
  1 => 
    array
      'post_id' => string '1248' (length=4)
      'posted' => string '2010-08-29 19:33:10' (length=19)
      'message' => string '+1' (length=2)
      'author' => string '34093' (length=5)
      'u' => 
        array
          'user_id' => int 34093
          'avatar' => boolean true
          'name' => string 'Артемий Комаров' (length=29)
  ...

Что получилось

Мы добились двух приятных вещей.

Во-первых, избавились от JOIN и переложили часть работы на куда более масштабируемый Memcached. Если возрастут нагрузки, железо нам скажет только спасибо.

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

На начальном этапе, если не хочется ставить мемкэш, можно его даже выкинуть, заменив на объект, который на все get() выдаёт, что такого ключа нет. Когда же производительности не будет хватать, просто добавить его, прозрачно для всего внешнего кода.

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

Тесты производительности

Простой тест на моём хиленьком нетбуке с Ubuntu 9.10 и MySQL 5.1.

Выборка 20 сообщений сразу с пользователями (JOIN)

Первый запрос — 250 мс, второй сразу — 5 мс, запрос для другой темы (`topic`) — опять 250 мс.

Как видно MySQL-кэш здесь нам здорово помогает.

Наш способ — пустой Memcache

Сначала достаём сообщения: SELECT ... FROM `posts`13 мс с первого раза и 1 мс со второго.

Затем multi-get, возвращающий пустой массив — 2 мс.

Затем выборка из базы SELECT ... WHERE `user_id` IN (...)15 мс и 1,5 мс.

И multi-set полученных данных в кэш — 2 мс.

Итого — 32 мс без mysql-кэша и 6,5 с ним.

Наш способ — половина пользователей есть в Memcache

Абсолютна та же последовательность и те же результаты.

Наш способ — полный Memcache

Отсутствуют SELECT ... WHERE ... IN (...) и multi-set.

15 мс без mysql-кэша, 3 с ним.

Итоги

Итак, при одиночных запросах, работающем mysql-кэше и хлипеньком Memcached, мы по крайней мере не проиграли в производительности, хотя вместо одного запроса используем несколько да ещё и к разным хранилищам. Примерно равная производительность, но лучшие интерфейс, модульность и масштабируемость.

При отсутствии кэша в mysql (а при большом количестве разнообразных запросов особенно на него расчитывать не стоит) — 250 мс с JOIN’ом явно склоняют чашу в нашу сторону.
Плюс, когда дойдёт до параллельных запросов, JOIN вряд ли покажет себя лучше.

Более вменяемые тесты приветствуются.

P.S.

Как я рад, что уже не надо ни в школу, ни в универ :)

36 комментариев »

  • Пачиму код нерасцвечивается, читать невазможна, дорогой!

    А во вторых, хотелось бы цифирки в производительности решения — до и после.

    Горбунов Олег, 31.08.2010, 12:47

  • >> выкинуть, заменив на объект, который на все get() выдаёт, что такого ключа нет
    ога. будем вызывать по отдельному MySql SELECT для каждого пользователя. отличная оптимизация!

    а что если после чтения постов просто сделать второй и последний запрос к MySql
    SELECT u…. WHERE u.id IN (список userid).
    список userid нам известен. от JOIN мы избавляемся. разве это не решение задачи? работать будет быстро

    artoodetoo, 31.08.2010, 12:54

  • сорри, читал невнимательно :(((

    artoodetoo, 31.08.2010, 12:56

  • Круто! Меня тоже постаянно напрягала проблема связи разных модулей, тех же пользователей с какой-то другой штукой.
    Но я наверно то же как и artoodetoo невнимательно читал. Как насчет двух запросов без JOIN?

    kostyl, 31.08.2010, 13:51

  • Горбунов Олег, расцвечивать код — влом, цифорки — тоже влом, но сделаю :)

    artoodetoo, :)
    Кстати, без мемкэша тот же самый запрос в итоге и получится.

    kostyl, подробнее про два запроса.

    vasa_c, 31.08.2010, 14:39

  • два запроса
    один — выбрать посты
    втрой — выбрать пользователей по whereIn(id пользователей, написавших посты из первого запроса)

    циферок про производит. не хватает.

    И день знаний завтра же, а сегодня 31 авг.

    Абырвалг, 31.08.2010, 15:02

  • Кроме того: http://www.jwage.com/2010/08/25/blending-the-doctrine-orm-and-mongodb-odm/

    Абырвалг, 31.08.2010, 15:03

  • >два запроса
    Ну к этому в итоге и сводится, только данные из второго запроса кэшируются в мемкэше.

    >И день знаний завтра же, а сегодня 31 авг.
    Забыл, что в августе 31 день. Ничего, завтра исправлю дату поста :-D

    vasa_c, 31.08.2010, 15:25

  • Добавил в конце тесты.

    vasa_c, 31.08.2010, 16:37

  • Мне все равно доктриновская реализация больше нравится. А тут статика, перечисление полей в константе. Некрасиво как-то.

    Хотя в примере от jwage связь 1:1.

    Абырвалг, 31.08.2010, 17:20

  • кстати, я тоже поднимал подобную тему
    http://pyha.ru/forum/topic/4495.0

    Абырвалг, 31.08.2010, 17:29

  • Доктрина — ОРМ, это вообще из другой плоскости.

    А у меня здесь реализация так просто, для наглядности примера. Статейка то не про неё, а про сам алгоритм.

    vasa_c, 31.08.2010, 17:43

  • ну да. Мне кажется, что ОРМка с поддержкой кеша неплохо поможет тебе.

    По поводу модульности:
    предположим, что к юзерам нужно еще добавить фотгу (не аватарку). Псевдокод:


    $posts = $em->create('forum\posts'); // создали экземпляр queryBuilder'а
    $ed->notify('forum.posts.preQuery', $posts); // отправили наш экземпляр QB eventDispatcher'у
    $posts = $posts->get(); // собсно говоря получили наши сообщения
    $ed->filter('forum.posts.postQuery', $posts); // а может быть полученные сообщения нужно еще как-то фильтрануть, напр. прописать абс. пути к фоткам

    // в плагине фоток, который отработает по forum.posts.preQuery
    $posts->related('photos\users'); // джоиним фотги. Связь 1:1, это прописано в модельке photos\users

    Абырвалг, 31.08.2010, 20:24

  • Мне уже ничего не поможет :)

    Всё-таки я про одно, а ты про другое.

    vasa_c, 31.08.2010, 22:10

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

    artoodetoo, 1.09.2010, 16:46

  • Правильные тесты и указание неточностей приветствуются.

    vasa_c, 1.09.2010, 16:49

  • a2d2 Все модели неверны, но некоторые из них полезны…

    kostyl, 1.09.2010, 17:24

  • А вот скажите мне, профи, как грамотно сделать max() и «>=» для хранилищ типа key-value?

    artoodetoo, 7.09.2010, 11:55

  • Обосрались?

    artoodetoo, 7.09.2010, 11:58

  • artoodetoo
    А зачем так делать для хранилищ такого типа?

    kostyl, 9.09.2010, 0:46

  • Можно не делать. Особенно если это невозможно :) Я думал над тем чего было бы выгодно закешировать в memcache или в redis и возникла такая потребность.

    Пример 1: курсы валют или любые другие «ставки по времени». Чтобы узнать курс на дату D надо найти максимальную дату <= D на которую была запись

    Пример 2: постраничный вывод очень больших списков. LIMIT from, size очень накладная операция в mysql. Можно было бы кешировать id первой записи на странице. Обратный расчет номера страницы реализуется как с курсами.

    Пример 3: у нас закешированы страницы. В большом списке удалена запись с неким id. Очевидно что это повлияет на нумерацию страниц, соответствующих id с бОльшими значениями

    artoodetoo, 9.09.2010, 6:34

  • >Обосрались?
    Нет, просто за грибами ездил.
    Общий ответ — никак. А для конкретного хранилища — хз.

    vasa_c, 9.09.2010, 18:11

  • artoodetoo, http://pyha.ru/wiki/index.php?title=Redis:type-zset — упорядоченные множества в Redis, можно делать аналоги индексов в MySQL, ну и максимальные даты, минимальные и т.п.

    vasa_c, 10.09.2010, 12:34

  • Странно, а почему у вас для поля ‘topic’ не указан индекс. А вроде так умно выбираете по нему записи из таблицы? Что-то я сомневаюсь в том, что это тот пример в котором join будет долго работать. Кроме того индекс по author тоже поможет.

    legion, 7.11.2010, 0:57

  • legion, там несомненно есть индексы, без них было бы совсем печально. Они скрыты под строчкой «— ..», как и не относящиеся к задачи поля.
    Ключ по `topic`, видимо, следует указать, чтобы не вводить в заблуждение.

    vasa_c, 7.11.2010, 12:39

  • Ах-ха-ха. Зачем же индекс-то не в ту таблицу дописали? Речь шла о таблице `posts`, а не о `users`.
    И даже не `topic` важнее, а именно `author` по которому делается join.

    legion, 7.11.2010, 14:19

  • legion ну ты гонишь, статья не про индексы, тут и так все знают какие индексы куда и зачем ставить, суть не в них…

    kostyl, 7.11.2010, 14:37

  • kostyl стоит ли городить огород там где проблем быть не должно? Если бы JOIN выполнялся в десять раз медленнее чем два запроса, то его бы вообще никогда не использовали бы. А именно на производительность давит автор.

    legion, 7.11.2010, 15:33

  • >Ах-ха-ха. Зачем же индекс-то не в ту таблицу дописали? Речь шла о таблице `posts`, а не о `users`.
    Потому что воскресенье.

    >И даже не `topic` важнее, а именно `author` по которому делается join.
    Мотивируйте )

    vasa_c, 7.11.2010, 15:46

  • >А именно на производительность давит автор.

    Автор давит на то, что без JOIN’а в данном случае проще, лучше, светлее и получается более гибкая архитектура.

    Сторонники же JOIN’а давят на то, что JOIN намного производительнее. Для многих догма, что «обработка данных должна всецело быть в БД», «один запрос всегда быстрее нескольких делающих тоже самое, тем более если там ещё вклинится PHP».

    Автор пытается сказать, что это не совсем так :)

    >Если бы JOIN выполнялся в десять раз медленнее чем два запроса, то его бы вообще никогда не использовали бы.

    Смотря где. На больших нагрузках могут быть ситуации и в 10 и в 100 раз.

    vasa_c, 7.11.2010, 15:52

  • Если некоторые люди не видят где-то проблемы, это совсем не значит, что её нет, да и всё…

    kostyl, 7.11.2010, 15:55

  • Наверно скорость работы запроса зависит не от нагрузки, а от объема данных?
    Если ваш тест проводился под нагрузкой, то позвольте спросить чем вы мерили время?
    Миллисекунды показал вам сервер mysql или выступающий в роли клиента php?

    Написать много строчек кода вместо одной это действительно проще. :)

    legion, 7.11.2010, 16:13

  • kostyl вспоминается старая байка про то, что в невесомости шариковые ручки не пишут. И что американцы потратили на решение проблемы миллиард долларов, а русские как писали так и продолжали писать карандашом. Проблема есть, только подумайте какой вы результат хотите получить решая её.

    legion, 7.11.2010, 16:18

  • >Наверно скорость работы запроса зависит не от нагрузки, а от объема данных?
    Скорость работы зависит много от чего.
    Если у вас много параллельных запросов, а JOIN блокирует таблицы, это может повлиять очень существенно.

    >Если ваш тест проводился под нагрузкой, то позвольте спросить чем вы мерили время?
    Нет, конкретно этот тест проводился без нагрузки и вообще он предельно простой.
    Если вы сделаете тест получше и прогоните его под нагрузкой, это позволит получить цифры интересные нам всем :)

    >Написать много строчек кода вместо одной это действительно проще. :)
    Это основа программирования — подпрограммы, модули, инкапсуляция )
    С одной стороны кажтся, что 20 строчек больше чем 5, вот только эти 20 лежат в одном месте, а те 5 в сотне мест, итого их 500.
    И когда потребуется изменить, придётся искать и исправлять во всех местах.

    vasa_c, 7.11.2010, 18:00

  • я знаю, что у нас была проблема с выборкой с помощью JOIN, правда на Firebird и все закрывали глаза на эту вещь несколько лет. Но вот я такой крутой чувак )), немного поразмыслив понял, что можно явно определить пять типов данных и сделать 5 обычных запросов без JOIN точно также в итоге получив все нужные данные. Прикол в том что никогда не понадобится делать 6, или потом 7 запросов. В итоге углядев «действительную реальность» удалось на 70% увеличить быстродействие, что позволило не терять 25% клиентов… Проблема не в JOIN была ведь..

    kostyl, 7.11.2010, 23:33

  • Хуйня какая то че за сука урод это пишет, нихуя понятного. Ну вы далбоебы!!!!!!!!!!!!!!!!

    Ka4aH, 15.03.2011, 8:55

Leave a comment