MySQLda ORDER BY siz ma'lumotni saralaymiz

Salom. Agar kimdakim DB (Database, Ma'lumotlar bazasi) bilan ishlagan bo'lsa, demak ko'pchiligik sarlash (sortirovka)ga duch kelgan. Oddiy SQL tilida ORDER BY. Lekin bilamizki bu ORDER BY juda sekin ishlaydi. Buni tezlashtirish yo'llari ham mavjud. Keling shu haqida gaplashsak.

Mani yozmoqchi bo'lganlarim MySQL misolida. Lekin bu usulni boshqa DB larga ham o'tkazsa bo'ladi. Keling man misol sifatida social network (sotsial tarmoq) ni oliy. Ya'ni undagi ichidagi funksiyalarning bazada strukturasi haqida gaplashamiz.

Endi asosiy maqsadga o'tkas, ORDER BY ni tez ishlaydigan holatga olib kelish.

1-holat. MySQL ning o'zida ORDER BY ni tezroq ishlatish uchun index lardan foydalansa bo'ladi. Ya'ni ayrim hollarda, jumlada:
SELECT * FROM t1
  ORDER BY key_part1,key_part2,... ;

SELECT * FROM t1
  WHERE key_part1 = constant
  ORDER BY key_part2;

SELECT * FROM t1
  ORDER BY key_part1 DESC, key_part2 DESC;

SELECT * FROM t1
  WHERE key_part1 = 1
  ORDER BY key_part1 DESC, key_part2 DESC;

SELECT * FROM t1
  WHERE key_part1 > constant
  ORDER BY key_part1 ASC;

SELECT * FROM t1
  WHERE key_part1 < constant
  ORDER BY key_part1 DESC;

SELECT * FROM t1
  WHERE key_part1 = constant1 AND key_part2 > constant2
  ORDER BY key_part2;

Misollar dev.mysql.com/doc/refman/5.0/en/order-by-optimization.html dan olindi. Ya'ni saralanishi lozim bo'lgan ustun index ko'rinishida yaratiladi. Lekin bunda ham qandaydir chegaralar mavjud. Ya'ni tepada ko'rsatilgan holatlardagina INDEX dan foydalaniladi. Aks holda oldingiday holatda ishlatiladi.

Bu haqida MySQL saytlarida va ya'na bir qancha internetdagi resurslardan qidirib topsangiz bo'ladi.

Keling endi man qanday yo'l tutganim haqida yozsam. Yaqinda ayrim sabablarga ko'ra social network yozishni boshladim. Man ishni boshlashdan oldin manga juda tez ishlaydigan DB kerak bo'ldi. Qidirib qidirib ya'na MySQL da qoldim. Chunki MySQL da opennet.uz/itnews/mysql-memcache-ni-ham-orqada-qoldirdi.html maqolada yozganimda HandlerSocket pluginini topdim. Bu yerdagi eng katta hususiyat INDEX larda juda katta tezlikda ma'lumotni qidirb topib beradi. Demak DB shunday tuzish kerakki barcha jadvallar INDEX lardan iborat bo'lishi va manga kerak holatda ishlashi lozim edi.

Birinchi chiqqan muammo lenta novosti. Ya'ni o'zingiz yozgan post sizning lentangizda va do'stlaringizning letasida chiqishi kerak. Chiqqandayam qo'shilgan vaqti bo'yicha kamayish tartibida bo'lishi lozim. Umuman
SELECT * FROM user_newsfeed WHERE id_user = {id} ORDER BY datetime_added DESC LIMIT {n},{m}

Bu oddiy SQL tilida yozilishi, lekin man topgan plugin ORDER BY ni qo'llamaydi. Man boshqacharoq yo'lga o'tishim kerak bo'lib qoldi. Ochig'i ko'p o'yladim va quyidagi yechimni topdim.

users jadvaliga user_post_count, newsfeed_post_count ustunlarini qo'shdim. Birinchi ustun user nechta post qo'shganligi haqidagi ikkinchisi userning lentasida netchta post borligini ko'rsatadi.

Va qolgan jadvallar:
CREATE TABLE IF NOT EXISTS `posts` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `id_user` bigint(20) unsigned NOT NULL,	
  `type` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `text` text NOT NULL,
  `post_info` text,
  `datetime_added` datetime NOT NULL,
  `user_post_counter` bigint(20) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

CREATE TABLE IF NOT EXISTS `user_newsfeed` (
  `id_user` bigint(20) NOT NULL,
  `counter` bigint(20) NOT NULL,
  `id_post` bigint(20) NOT NULL,
  `id_from` bigint(20) unsigned NOT NULL,
  `datetime_added` datetime NOT NULL,
  KEY `POST_COUNTER` (`id_user`,`counter`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `user_posts` (
  `id_user` bigint(20) unsigned NOT NULL,
  `counter` bigint(20) unsigned NOT NULL,
  `id_post` bigint(20) unsigned NOT NULL,
  `datetime_added` datetime NOT NULL,
  PRIMARY KEY (`id_user`,`counter`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


Ushbu jadvallarga izoh keltirsam.

`posts`:
id — bu tushinarli, takrorlanmas son ya'ni post id si
id_user — postni kim qo'shgani

`user_newsfeed` — asosiysi
id_user — kimga qo'shilgan (ya'ni ID=1 bo'lgan user letasidagi postlar)
counter — ushbu userning nechanchi posti (batafsil pastroqda)
id_post — qaysi post, post id
id_from — kim tomonidan

`user_posts`:
id_user — kim qo'shganligi
counter — ushbu userning qo'shgan postlariga qarab INCREMENT bo'ladi. Agar 10 ta post qo'shgan bo'lsa, 1-10 gacha qiymat oladi.
id_post — post id si

Qolganlari qo'shimcha ma'lumotlar. Jadvallarga e'tibor bering, ayniqsa KEY va PRIMARY KEY larga.

Endi tasavvur qiling sizning 10 ta do'stingiz bor. Agar siz bitta post yozsangiz, siz yozgan post birinchi `posts` jadvaliga tushadi, u yerda ID si olinadi va `user_post_count` qiymati bittaga oshirilib `user_posts` jadvaliga yoziladi `user_post_count` oshgandagi qiymati `counter` ga yoziladi va `user_newsfeed` jadvaliga quyidagicha qo'shiladi.

sizni va do'stlaringizni ID lari bitta o'zgaruvchi ($ids = array(1, 2, 3, ..., 11) ga olinadi va har bir userning `newsfeed_post_count` qiymati bittaga oshirilib `user_newsfeed` jadvaliga qo'shiladi. Bunda har biriga mos holda `counter` o'zgaruvchisi `newsfeed_post_count` ning oxirgi qiymatini saqlaydi. Misol tariqasida: id_post = 11 bo'lsin

Userlar
id=1, newsfeed_post_count = 5
id=2, newsfeed_post_count = 9

bo'lsa shu ikkalasiga `user_newsfeed` quyidagi ko'rinishda bo'ladi
id_user, counter, id_post
1	6	11
2	10	11

va userlar
id=1, newsfeed_post_count = 6
id=2, newsfeed_post_count = 10

ko'rinishga o'tadi.

Bu yerda ya'na bitta muammo bor.

Endi tasavvur qiling, uch kichi bir biri bilan do'st. ID lari 1,2,3 bo'lgan userlar o'zaro do'st. Ya'ni 1-2,3 bilan, 2-1,3 va 3-1,2 bilan. Agar mani holatimda uchala user birdaniga post yozsa nima bo'ladi?

Kelin sekin qadamma-qadam tushintiray. Umuman PHP dagi kod quyidagicha
public static function insertPost($text, $values = array())
{
	//o'zining letasiga qo'shish
	list($pid, $post_count) = self::insertPostPerUser(Yii::app()->user->user, Yii::app()->user->id, $text, $values);

	//do'stlarini olish
	$friends = UserFriendsModel::hsFindAllByIndex(Yii::app()->user->id, 'id_user2,status', 0x00FFFFFF);

	if ($friends)
	{
		$values['post_id'] = $pid;

		foreach($friends as $friend)
		{
			//agar do'sti bo'lmasa qo'shmaydi
			if ($friend->status != 2)
				continue;

			//do'stlariga qo'shish
			self::insertPostPerUser($friend->id_user2, Yii::app()->user->id, $text, $values);
		}
	}

	//user_posts ga qo'shish
	$lockKey = "user_post_".Yii::app()->user->id;
	if (MutexHelpers::lock($lockKey, 5))
	{
		$user = UsersModel::hsFindByIndex(Yii::app()->user->id, "id,user_post_count");
		$user_post_count = $user->user_post_count + 1;

		UsersModel::hsUpdate($user->id, array(
			"user_post_count" => $user_post_count
		));

		UserPostsModel::hsInsert(array(
			"id_user" => $user->id,
			"counter" => $user_post_count,
			"id_post" => $pid
		));

		MutexHelpers::releaseLock();
	}

	return $post_count;
}

//har bitta user uchun post qo'shadi
public static function insertPostPerUser($id, $id_from, $text, $values)
{
	$post_count = false;
	
	$lockKey = "user_".$id;
	if (MutexHelpers::lock($lockKey, 5))
	{
		$user = UsersModel::hsFindByIndex($id, "newsfeed_post_count");
		$post_count = $user->newsfeed_post_count;

		$post_count += 1;

		UsersModel::hsUpdate($id, array(
			"newsfeed_post_count" => $post_count
		));

		MutexHelpers::releaseLock();
	}

	$values['id_user'] = $id;
	$values['text'] = $text;

	//birinchi savar qo'shilganda `posts` ga qo'shadi
	if ( ! isset($values['post_id']))
		$pid = PostsModel::hsInsert($values);
	else
		$pid = $values['post_id'];

	UserNewsfeedModel::hsInsert(array(
		"id_user" => $id,
		"counter" => $post_count,
		"id_post" => $pid,
		"id_from" => $id_from
	));

	return array($pid, $post_count);
}


Agar kodni o'rgangan bo'lsangiz MutexHelpers mavjud, bu haqida sal pastroqda tushuntiraman. Endi muammoga qaytamiz, ya'ni uchala user birdaniga post yozgandi. Birdaniga bu, soati, sekundi, milli sekundi bir vaqtda bo'lgan vaqtdi. Bu holat ehtimoldan ancha uzoq lekin do'stlaringiz ko'paygani sari bu ehtimol oshib boradi. Biz dasturchilar esa 0 dan katta ehtimollikka ega har qanday vaziyatni oldini olish lozim.

Demak uchala user birdaniga post yozishdi va birdaniga ENTER ni bosishdi. PHP kod qatorma-qator ishlab bizni quyidagi kodga kelishdi (uchala sessiya ham)
...
1.	$lockKey = "user_".$id; //uchala sessiya ham shu yerda turibdi
2.	$user = UsersModel::hsFindByIndex($id, "newsfeed_post_count");
3.	$post_count = $user->newsfeed_post_count;
	...

Hozircha MutexHelpers bizga shartmas. Endi uchala sessiya uchun ham DEBUG qilgandan qadamma-qadam pastga tushsak, 2-qatorda $id bo'yicha userning `newsfeed_post_count` qiymatini olamiz. Uchalasi bir biri bilan do'stligi uchun $id bir xil bo'lish ehtimoli bor. Demak $id uchla sessiya uchun ham bir xil bo'lsin. Shudan `newsfeed_post_count` qiyma uchala sessiya uchun ham bir xil qiymat ya'ni `users` jadvalidagi `newsfeed_post_count` qiymatni olishadi va uchlasi ham bittaga oshirib yana `users` jadvalga `newsfeed_post_count` + 1 (bir marotaba oshgan) qiymatni yozishadi. Aslida $id=1 bo'lgan userga 3 ta post qo'shilgandi. Bizni holatda `user_newsfeed`.`counter` ustunida dublikat paydo bo'lardi.

Endi bu muammoni oldini olish uchun LOCK tushunchasidan foydalanamiz. Ya'ni koddagi
$lockKey = "user_".$id;
if (MutexHelpers::lock($lockKey, 5))

kodni qo'shamiz. Ushbu kodni quyidagicha tushuntirsam. Bizni holatda $id=1 edi, demak uchala sessiyada ham $lockKey = «user_1» bo'ladi. MutexHelpers::lock bo'lsa, «user_1» bo'yicha birinchi sessiyani (umuman ketma ketlikdagi birinchi sessiyani) ga ruxsat berib ikkinchi va uchinchi sessiyalar «user_1» bo'lganligi uchun shu yerda kutib turishadi. To 1-sessiya
MutexHelpers::releaseLock();

ni chaqirmaguncha. Ya'ni shu joyga kelganda $lockKey ga qarab $id lar bir xil bo'lishidan qatiy nazar `counter` o'zgaruvchisi takrolanmasdan INCREMENT bo'lib ketaveradi. MutexHelpers esa quyidagicha
class MutexHelpers
{
    public static $lockKeyStack = array();
    public static function lock($key, $timeout)
    {
        self::$lockKeyStack[] = $key;
        return Yii::app()->db->createCommand("SELECT COALESCE(GET_LOCK('$key', $timeout), 0)")->queryScalar();
    }

    public static function releaseLock($key = false)
    {
        if ($key === false)
            $key = array_pop(self::$lockKeyStack);

        return Yii::app()->db->createCommand("SELECT RELEASE_LOCK('$key')")->queryScalar();
    }
}

Hozircha PHP da multi session lock bo'yicha faqat shu usulni ishlayapman. Bundan tashqari va tezroq ishlaydigan biblatekalar mavjud. PHP Semaphore, PHP shmop lardan foydalanib ham shu natijaga erishsa bo'ladi.

Keling endi asosiy muammiz ORDER BY ga qaytamiz. Agar maqolani e'tibor bilan o'qigan bo'lsangiz manimcha allaqachon tushinib bo'lgansiz. Endi man lentani o'qishda quyidagicha usuldan foydalandim.
Birinchi = `users` jadvalidan `newsfeed_post_count` ni qiymatini olaman va $pageSize = 10 (ya'ni o'ntadan kor'satish kerak bo'lsa) va $start = $newsfeed_post_count, $end = $newsfeed_post_count — $pageSize. Bundan
for($i = $start; $i > $end && $i > 0; $i--)
{
	$user_newsfeed = UserNewsfeed::hsFindByIndex(array($id_user, $i)); //$i - bu yerda counter qiymati
	$post = PostsModel::hsFindByIndex($user_newsfeed->id_post);
	...
}


Ko'rib turganingizday sartirovkani oldindan belgilab ketdik. Ya'ni o'zimiz o'sib boradigan o'zgaruvchi yaratdik. Shu usuldan foydalanib boshqa muamolaringizni ham hal qilsangiz bo'ladi. Bu usulni bitta kamchiligi. Agar `user_newsfeed` jadvalidan ma'lumotlar o'chiriladigan bo'lsa `counter` qiymati orasida bo'shliq paydo bo'ladi. Ya'ni 1,2,3,5,8,11 ko'rinishida bo'lishi mumkin.

Lekin facebook, odnoklassnikida o'zini statusini o'zgartirgan yoki post yozgan inson deyarli shu postni o'chirmaydi. Shuni e'tiborga olib ushbu usul yordamida muammoni hal qildim. Va HandlerSocket ning ORDER BY ni yo'qligini hisobga olib ham shuning bor imkoniyatindan to'liq foydalandim va foydalanyapman ham.

Ya'na HandlerSocket ni ishlatmasdan ham shu usulda, ya'ni INSERT kam bo'ladigan lekin SELECT ko'p ishlatiladigan jadvallarda ORDER BY ni ishlatmasdan
SELECT * FROM {table} WHERE id_user = 1 AND `counter` IN (1, 2, 3, ..., 10)

Ko'rinishida yozib PHP da sort, ksort lardan foydalansaigiz yoki MySQL ning o'zidagi ORDER BY FIELD(`counter`, 1, 2, 3,… 10) dan foydalansangiz bo'ladi. Bu ancha tez ishlaydi. Chunki ORDER BY butun boshli jadvalni sartirovka qilmaydi. Agar jadvalda ma'lumot juda katta bo'lasa albatta.

Ochig'i oxirgi kunlarda 2-3 ta proyekt qilib DB da har qanday muammoni hal qilish mumkinligiga ishonch hosil qilyapman. Faqat hamma jadvallarni to'g'ri yarata olish lozim.

Agar tushunmovchiliklar bo'lsa kutaman.

6 комментариев

Комментарий был удален
mageUz
Ha, ko'p sonli ma'lumotlar bilan ishlaganda, bazada bir qancha muammolar yuzaga keladi. Qanday qilib tezroq ...? degan savolga albatta proektni qurishdan avval javob topib kerakli texnologiyalarni o'rganib chiqish kerak. Hozirda MySql ni kerakli darajadagi tezligini ta'minlashda biz bilgan va bilmagan juda ko'plab sozlanmalari mavjud, oddiygina jadvalni yoki butun boshli baza arxitekturasini qurishdan tortib, server configuratsiyasi, master-slave nodlar bilan ishlash, keshlash kabi texnologiyalarni ham o'rganib chiqishga to'g'ri keladi. Menda maqola bo'yicha quyidagi fikrlar tug'ildi:
1. 10 ta user bir paytda millisekundigicha aniqlikda post qo'shish extimolini ko'rib chiqilibdi, bu yaxshi, lekin extimolligi juda ham past, agar bo'lganda ham kimdan keyin kim post qo'shganini userga qizig'i yo'q, aytmoqchi bo'lganim ular millisekundgacha aniqlikni sezmaydi ham, masalan mendan 1 millisekund keyin do'stim post qo'shdi nima, 1 millesekund oldin, farqi yo'q, muhimi post menga ko'rinsin, nari borsa minutlar farqi meni qiziqtirishi mumkin. Lekin yaxshi juda past extimollikni inobatga olingani.
2. Agar post o'chirilsa, yoki user do'stlar safidan o'chirilsa nima bo'ladi degan savol, bu yerdagi logika bo'yicha counterlarni kamaytirib chiqish kerakmi?
3. Agar order by qo'llanilmasa, user_newsfeed dagi qatorlar row_id qo'shilish tartibida select bo'ladi, Limit yordamida kerakli qatorlarni olsa bo'lmaydimi?
4. KEY `POST_COUNTER` (`id_user`,`counter`) index uchun unikal bo'lmagan qiymat berish extimolichi?
5. If(MutexHelpers::lock($lockKey, 5)){....} qatorga bir vaqtda kelib qolgan bir nechta userdan faqat bittasi shartni ichiga kiradi, qolganlari kutib turadimi yoki shartni bajarmay o'tib ketadimi? Shu logikani shared memory lar bilan hal qilsa bo'ladimi (APC yoki Memcache)
6.
for($i = $start; $i > $end && $i > 0; $i--)
{
        $user_newsfeed = UserNewsfeed::hsFindByIndex(array($id_user, $i)); //$i - bu yerda counter qiymati
        $post = PostsModel::hsFindByIndex($user_newsfeed->id_post);
        ...
}


Bu yerda sikl ichida bazaga so'rov berilyabdimi? Agar shunday bo'lsa nima uchun?
1
shranet
1. Ehtimollik bor. Agar sizda 120 ta do'stingiz bo'lsa (Facebookdagi o'rtachasi shu), ehtimoli bor bir vaqtda 10 ta do'stingiz post yozishini. Agar `counter` qiymati shu 10 ta post uchun bitta qiymat bo'lsa, manda pagination qiganda pagedagi sonlari har xil bo'ladi.

2. Man lenta novostini shundan usulda qildimki, do'stlari o'chsayam, lenta novostidan post o'chmaydi. Chunki shu paytda siz u bilan do'st bo'lgansiz (Manga shu osonroq bo'ld). Agar post o'chsa maqolada yozganimdek `counter` qiymati oralig'ida bo'shliq paydo bo'ladi.

3. Manga teskari tartibda kerak, man bu usulni ham ko'rdim. Lekin ishlata olmadim. Yoki HandlerSocket SELECT ni boshqacha usulda qiladi.

4. Bersa bo'ladi, lekin mandagi ayrim hollarda `counter` qiymati bir xil bo'lishi mumkin edi. Hozircha uni hali yozganim yo'q.

5. MutexHelpers::lock($lockKey, 5) $keyLock — bir xil bo'lganda qolganlari 5 sekungacha kutib turadi. Buni ochirish mumkin. Lekin o'rtacha do'stlaringiz 120 ta bo'lishini hisobga olsangiz, serverlar 5 sekund ichida be'malol qo'shishga ulguradi. Shared memorylar orqali qilsa bo'ladi. Hozircha u haqida bosh qotirib ko'rmadim.

6. Agar HandlerSocket maqolasini o'qigan bo'lsangiz u tez ishlashi uchun barcha DB RAB da joylashishi kerak. Shu holatdagina tezlik juda baland bo'ladi. Ha bular bazaga so'rov.
0
Surayyo
Assalomu alaykum. Manda bitta muommo: databaseda string bo'lgan columnda 08434590320\r+ shunaqa qiymat set qilingan, lekin aslida 08434590320 saqlangan. '\r+' sababli jsp shu nomer uchun oddiy function lar ham ishlamayapdi. :(
0
geniuz
String.trim bilan tozalavorsangiz ishlaydi,
bunaqa mavzuga unchalik aloqasi bulmagan savollaringiz bulsa bemalol forum.opennet.uz ga yozishingiz mumkin.
0