webForumDet fria alternativet

Hämta en hel gren från en kategoritabell?

Databaser & SQL

19 svar · 2 421 visningar · startad av bassebhu

Medlem sedan nov. 20016 480 inlägg
Frågan#1

Hej alla glada!

Jag har nu en tabell full med kategorier i totalt tre nivåer. Hur gör jag för att hämta alla poster för en hel gren?

Min tabellstruktur:
id - parent - title

Låt säga att en "gren" ser ut så här:

Huvudkategori 1 ... (id: 1, parent: 0)
--Subkategori 1 ... (id: 2, parent: 1)
----Kategori 1 ... (id: 3, parent: 2)
----Kategori 2 ... (id: 4, parent: 2)
--Subkategori 2 ... (id: 5, parent: 1)
----Kategori A ... (id: 6, parent: 5)
----Kategori B ... (id: 7, parent: 5)

Hur hämtar jag ut alla kategorier för den? Problemet är ju att alla underkategorier inte har id 1 som parent, utan nivå tre har nivå två som parent. Det räcker om den sträcker sig till nivå tre om det är lättare, men min struktur är tänkt att funka för obegränsat med nivåer så om det finns en lösning som hämtar rätt oavsett om det inte finns några undernivåer eller om det finns massvis så är det ett stort plus. Viktigt är dock att det går att hämta grenen både från roten (huvudkategori) eller från en "kvist" (subkategori).

Förslag?

SELECT * FROM kategorier
WHERE parent = $id OR [i]parentsparent[/i] = $id

parentsparent är inget som finns, men jag försöker beskriva det jag vill åt :)

Tack!

Medlem sedan juni 201161 inlägg
#2

Obegränsat med nivåer? Då vet jag inget annat sätt än att använda Common Table Expressions. Vilken databasmotor använder du? Har den stöd för CTE?

Medlem sedan nov. 20016 480 inlägg
#3

Ingen aning.

MySQL 5 och InnoDB.

Men om man satsar på två nivåer då (den tredje behöver nog inte vara sökbar så)? Parent och parents parent. :)

Medlem sedan juni 201161 inlägg
#4

Jag kollade på MySQL och CTE (eller Derived table WITH clause, som det officiella namnet är) och tydligen lades det in en Feature Request för detta i januari 2006. Men vad jag kan se har den inte ens blivit prioriterad ännu. Så där verkar det vara kört. :-(

Återstår alltså att hårdkoda antalet nivåer. Hur tänkte du dig att få resultatet presenterat? Bara de råa posterna från den grenen?

Medlem sedan nov. 20016 480 inlägg
#5

Jag har löst det hehe, men det blev nog rätt grisigt ;)

Lösningen jag gjorde var att göra en if-check som kollar om 1) det är en huvudkategori, 2) det är en subkategori eller 3) det är en vanlig kategori.

I fall 1-2) så loopade jag ut en hel radda med "parent = X OR parent = X OR ..." och i fall 3) så hämtade jag helt enkelt där parent = X utan några OR.

Det funkar och verkar inte vara jätteslött så det duger åt mig! Tack för hjälpen!

Medlem sedan juni 201161 inlägg
#6

Okej. Annars tror jag den här funkar:

SELECT *
FROM kategorier k
WHERE id = $id
OR parent = $id
OR EXISTS(SELECT * FROM kategorier WHERE parent = $id AND id = k.parent)
Medlem sedan nov. 20016 480 inlägg
#7

Tack! Det ser ju onekligen rätt ut, men kruxet är att jag ska få in det i en JOIN med en annan tabell också :)

SELECT * FROM poster
LEFT JOIN kategorier k
ON poster.kategori = k.id
WHERE poster.kategori = $id
OR k.parent = $id
OR EXISTS(SELECT * FROM kategorier WHERE kategorier.parent = $id AND kategorier.id = k.parent)

Typ så?

Det kanske bör vara nån annan JOIN men. Jag joinar ju med k.id. Hur går det ihop med din kod?

Orkar du? Tack! :)

Edit/ I sammanhanget kan nämnas att poster.kategori aldrig innehåller id till en huvud/subkategori.

Medlem sedan juni 201161 inlägg
#8

Hmm, låt mig se om jag har förstått din datamodell rätt:
Din tabell "poster" innehåller ett antal poster där några (men inte alla) hör till en viss kategori. Denna kategori finns i tabellen "kategori" och har aldrig några underkategorier, dvs är alltid ett löv i hierarkin. Rätt gissat så långt?

Sedan är det lite otydligt vad du vill ha ut. Vad jag förstår vill du kunna ange id:t på en godtycklig kategori-nod i hierarkin och få ut alla poster som hör till denna eller någon underliggande kategori. Datat du vill ha av varje är samtliga fält från "poster" och motsvarande fält från "kategori". Stämmer det?

Medlem sedan nov. 20016 480 inlägg
#9

Nästan rätt. Varje post har alltid en kategori, ett löv. Det är egentligen posterna jag vill ha ut, från önskade kategorier.

Man ska kunna söka bland posterna och genom en select-box välja om man vill hämta ut samtliga poster i en 1) huvudkategori (ibland parent och ibland parents parent), en 2) subkategori (parent) eller direkt från ett 3) löv.

Jag har som sagt löst det, men det blev en if-lösning med tre val för varje 1-3) där 1) först loopade ut sina barn (subkategorier) som därefter loopade ut sina (löv). 2) fick bara en loop som listade alla löv och 3) hämtade direkt där parent = id.

Detta känns som en väldig omväg och jag fick som sagt fler och fler OR i min sql-fråga för alt. 1-2)

Det rullar som sagt på obehindrat, men jag tar gärna emot ett snyggare förslag! :)

Edit/ Kategorierna kommer i framtiden att vara hyfsat fasta och jag har ett till kategorifält som jag inte nämnt (fullösning), is_parent. Detta fält har 1 på alla kategorier som är huvudkategori eller subkategori och 0 på alla löv. Därav var det enkelt att göra min if-sats ovan. Jag kollade helt enkelt om "parent == 0 && is_parent == 1" då har vi en huvudkategori. Om "parent != 0 && is_parent == 1" så har vi en subkategori och om "parent != 0 && is_parent == 0" då har vi ett löv. Fullösning jag vet, men is_parent hjälpte mig oerhört då jag enkelt ville kunna kolla vilken typ av kategori det rörde sig om, speciellt i min select-box där jag ville skriva ut olika klassnamn för att formatera olika kategorier.

Medlem sedan juni 201161 inlägg
#10

bassebhu skrev:

Nästan rätt. Varje post har alltid en kategori, ett löv.

Okej. Det var ett antagande baserat på att du visade LEFT JOIN mellan poster och kategorier.

Det är egentligen posterna jag vill ha ut, från önskade kategorier.

Man ska kunna söka bland posterna och genom en select-box välja om man vill hämta ut samtliga poster i en 1) huvudkategori (ibland parent och ibland parents parent), en 2) subkategori (parent) eller direkt från ett 3) löv.

Prova då:

SELECT *
FROM poster
WHERE kategori IN (
  SELECT *
  FROM kategorier k
  WHERE id = $id
  OR parent = $id
  OR EXISTS(SELECT * FROM kategorier WHERE parent = $id AND id = k.parent)
)

där $id alltså innehåller id:t på den kategori-nod och neråt som du vill ha ut samtliga poster från.

Medlem sedan nov. 20016 480 inlägg
#11

Tack, men jag ska alltså skippa JOIN helt då? :)

Jag har lite annat lull-lull iofs, men jag tycker mig ha överfört din sql rätt. Jag får: "Operand should contain 1 column(s)" när jag lägger till din snutt. Vad kan det vara?

Medlem sedan nov. 20016 480 inlägg
#12

Så här skrivs den ut:

SELECT posts.*, posts_rel.*, persons.person, users.username, (SELECT COUNT(*) 
  FROM posts_rel 
  WHERE p_r_parent = p_id
) has_translation 
FROM posts 
LEFT JOIN posts_rel 
ON p_id = p_r_parent 
LEFT JOIN users 
ON p_creator = u_id 
LEFT JOIN persons 
ON p_person = p_id 
WHERE NOT (p_visibility = 0) 
AND p_kategori IN (
  SELECT * FROM kategorier k 
  WHERE k_id = 2 
  OR k_parent = 2 
  OR EXISTS(
    SELECT * FROM kategorier 
    WHERE k_parent = 2 
    AND k_id = k.k_parent
  )
)

(Samtliga mina JOINS kanske bör vara INNER eller bara JOIN eftersom det alltid finns nåt i båda tabellerna? Jag kan inte de olika joinsen än) :D

Skippar jag allt från: AND p_kategori IN... så funkar frågan. Annars ber den om 1 column.

Medlem sedan juni 201161 inlägg
#13

bassebhu skrev:

(Samtliga mina JOINS kanske bör vara INNER eller bara JOIN eftersom det alltid finns nåt i båda tabellerna? Jag kan inte de olika joinsen än) :D

Byt till INNER JOIN om du vet att du alltid har matchande poster. Ju mer du talar om för en relationsdatabas, desto bättre sökstrategi kan den komma fram till.

Skippar jag allt från: AND p_kategori IN... så funkar frågan. Annars ber den om 1 column.

Himla hjälpsamt av den att inte berätta vilken kolumn den saknar... :(
Om du bara kör:

SELECT posts.* 
FROM posts 
WHERE NOT (p_visibility = 0) 
AND p_kategori IN (
  SELECT * FROM kategorier k 
  WHERE k_id = 2 
  OR k_parent = 2 
  OR EXISTS(
    SELECT * FROM kategorier 
    WHERE k_parent = 2 
    AND k_id = k.k_parent
  )
)

vad säger den då?

Medlem sedan nov. 20016 480 inlägg
#14

Får fortfarande bara Operand should contain 1 column(s) :(

Kan jag utvidga:
mysql_query($sql) or die(mysql_error()) ?

Även detta ger felet:

SELECT posts.* 
FROM posts 
WHERE NOT (p_visibility = 0) 
AND p_kategori IN (
  SELECT * FROM kategorier
)
Medlem sedan juni 201161 inlägg
#15

Förlåt, jag är ju dum i huvudet. Försöker göra för många saker samtidigt idag.

Det skall ju naturligtvis vara SELECT k_id och inte SELECT * i sub-selecten. Alltså:

SELECT posts.*, posts_rel.*, persons.person, users.username, (SELECT COUNT(*) 
  FROM posts_rel 
  WHERE p_r_parent = p_id
) has_translation 
FROM posts 
LEFT JOIN posts_rel 
ON p_id = p_r_parent 
LEFT JOIN users 
ON p_creator = u_id 
LEFT JOIN persons 
ON p_person = p_id 
WHERE NOT (p_visibility = 0) 
AND p_kategori IN (
  SELECT k_id FROM kategorier k 
  WHERE k_id = 2 
  OR k_parent = 2 
  OR EXISTS(
    SELECT * FROM kategorier 
    WHERE k_parent = 2 
    AND k_id = k.k_parent
  )
)
Medlem sedan nov. 20016 480 inlägg
#16

AMAZING! :) Min kod blev 50 rader lättare ;)

Tack för all hjälp!

Medlem sedan nov. 20016 480 inlägg
#17

Hehe, jag hoppades att jag skulle slippa fråga igen, men nu har jag försökt i flera timmar och det blir bara sämre och sämre och sämre :P

Utöver tidigare listning så skulle jag nu vilja lista alla poster och placera dem i ul-listor under rätt kategorier. Tillbaka till ett liknande exempel som i första inlägget, men nu även med poster:

Huvudkategori 1
--Subkategori 1
----Kategori 1
------Post 1
------Post 2
----Kategori 2
------Post 3
--Subkategori 2
----Kategori 3
------Post 4
Huvudkategori 2
--Kategori 4
----Post 5
Huvudkategori 3
--Post 6

Posterna är alltid löv och jag vill inte visa huvud-/sub-/kategorier som inte har några poster. Mitt taffliga försök innehåller nu en hel drös med JOINS :(

SELECT posts.*, posts_rel.*,
kategorier.k_ordning, kategorier.k_titel, kategorier.k_id AS k_parent, kategorier.k_is_parent, 
kategorier_p.k_titel AS k_titel_p, kategorier_p.k_id AS k_parent_p, 
kategorier_p_p.k_titel AS k_titel_p_p, kategorier_p_p.k_id AS k_parent_p_p
FROM posts
INNER JOIN posts_rel
	ON p_id = p_r_parent
INNER JOIN kategorier
	ON p_kategori = k_id
INNER JOIN kategorier AS kategorier_p
	ON kategorier.k_parent = kategorier_p.k_id
INNER JOIN kategorier AS kategorier_p_p
	ON kategorier_p.k_parent = kategorier_p_p.k_id
WHERE person = " . $personid . "
ORDER BY k_order ASC, p_name ASC

Detta måste ju gå att lösa på något annat sätt? Jag låser åter igen in mig på att det bara funkar till nivå tre nu också ;)

Tanken med det här är att jag för varje postrad nu får fram post-namnet och alla huvud-/sub-/kategorier som tillhör varje post. Jag kan sen lista dem i rätt ordning med hjälp av fältet k_ordning och därefter i bokstavsordning p_name. Min tanke är att sen kolla om kategorin från föregående post redan har printats ut, i så fall skriver jag bara ut postnamnet, annars printar jag ut en ny kategorirubrik.

Men ärligt, hur korkat är inte denna lösning? :e

Tackar för hjälp i rätt riktning!

Medlem sedan nov. 20016 480 inlägg
#18

hehe, ok, håll i er! Nu har jag skridit till verket med min ovanstående kod och här följer tillhörande PHP-kod i samma kaliber!! Är jag inte söt? :e
Det flyter på förvånansvärt bra, men jag har bara drygt 100 poster i databasen än så länge också så ;)

Oerhört tacksam om någon kan leda mig i rätt riktning för att snygga till detta hehe.

$used_parent_parent_parent = NULL;
$used_parent_parent = NULL;
$used_parent = NULL;
$p_p_p_end_ul = false;
$p_p_end_ul = false;
$p_end_ul = false;
$str = "<ul id=\"list-posts\" class=\"ul-list\">\n";
foreach( $posts as $row )
{

	if( $used_parent != $row['k_parent'] && $p_end_ul == true ) { $str .= "</ul>\n"; $p_end_ul == false; }
	if( $used_parent_parent != $row['k_p_parent'] && $p_p_end_ul == true ) { $str .= "</ul>\n"; $p_p_end_ul == false; }
	if( $used_parent_parent_parent != $row['k_p_p_parent'] && $p_p_p_end_ul == true ) { $str .= "</ul>\n"; $p_p_p_end_ul == false; }
	if( $used_parent_parent_parent != $row['k_p_p_parent'] )
	{
		$str .= '<li class="main-cat">' . $row['k_p_p_title'] . "</li>\n<ul>\n"; $used_parent_parent_parent = $row['k_p_p_parent']; $p_p_p_end_ul = true;
	}
	if( $used_parent_parent != $row['k_p_parent'] )
	{
		$str .= '<li class="';
		if( $row['k_p_p_parent'] == 0 ) { $str .= 'main-cat'; } else { $str .= 'sub-cat'; }
		$str .= '">' . $row['k_p_title'] . "</li>\n<ul>\n"; $used_parent_parent = $row['k_p_parent']; $p_p_end_ul = true;
	}
	if( $used_parent != $row['k_parent'] ) { $str .= '<li class="cat">' . $row['k_title'] . "</li>\n<ul>\n"; $used_parent = $row['k_parent']; $p_end_ul = true; }
	$str .= '<li class="post">' . $row['p_title'] . $row['k_p_p_parent'] . "</a></li>\n";
}

$str .= "</ul>\n</ul>\n</ul>\n</ul>";
echo $str;

Oinbjudande kod att kolla in jag vet, sorry. :) Jag vet att detta inte är PHP-forum, men det är så direkt kopplat till tidigare kod så jag tycker det var bäst. Tack!

Medlem sedan juni 201161 inlägg
#19

bassebhu skrev:

Detta måste ju gå att lösa på något annat sätt? Jag låser åter igen in mig på att det bara funkar till nivå tre nu också ;)

Tanken med det här är att jag för varje postrad nu får fram post-namnet och alla huvud-/sub-/kategorier som tillhör varje post. Jag kan sen lista dem i rätt ordning med hjälp av fältet k_ordning och därefter i bokstavsordning p_name. Min tanke är att sen kolla om kategorin från föregående post redan har printats ut, i så fall skriver jag bara ut postnamnet, annars printar jag ut en ny kategorirubrik.

Men ärligt, hur korkat är inte denna lösning? :e

Tackar för hjälp i rätt riktning!

Jag ser inte riktigt vad du skulle ändra på. I SQL Server hade jag smackat upp det som XML direkt ur databasen, men MySql har väl inget sådant? Och då återstår ju bara att ta med kategoritexterna på varje rad, sortera och bygga upp hierarkin i kod.

Medlem sedan nov. 20016 480 inlägg
#20

Hehe, jamen dåså. Som sagt, det funkar och rullar på snabbt så det kanske får vara så då.

277 ms totalt · 4 externa anrop · v20260731065814-full.1dc6f849
132 ms — deklarationer (db)
0 ms — hämta statistik (cache)
140 ms — hämta tråd, inlägg och bilagor (db)
134 ms — ändringar (db)