[MySQL] Join & Umkreissuche :D

0ver0n

Grünschnabel
Hi,

habe mal eine schwierige Frage und zwar habe ich mal ein Script gemacht, was mir ein Planet raus sucht, auf dem keiner wohnt. Das ganze soll für mein zukünftiges BG sein. *g*

Soweit geht es schon, jedoch will ich noch schauen, um im Umkreis von einem Feld kein anderer Planet oder ein Schiff ist. Die Schiffe stehen im Table 'space', die Planeten im Table 'planeten'.

Mein bisheriger Query:
Code:
SELECT w.x, w.y
FROM world AS w
LEFT JOIN planeten AS p ON w.id = p.world_id
WHERE w.typ = 'p'
AND p.besitzer IS NULL
LIMIT 1

Also der Umkreis müsste ja folgendes sein:
(w.x-1) bis (w.x+1) && (w.y-1) bis (w.y+1)

Kann man dem jetzt sagen, im Umkreis soll er keine Schiffe und Planeten ausgeben? Oder besser, er soll mir alle freien Felder anzeigen, die es gibt von allen Planeten(Umkreis 1Feld). Das wäre natürlich das Beste ;)



Christian
 
Hallo,

in deiner World-Tabelle gibt es für jede Position einen Eintrag? Und ansonsten ist dort markiert, ob es auf der Position ein Schiff oder ein Planet ist?

Dann kannst du es eventuell so lösen:

SQL:
SELECT w.x, w.y
  FROM world AS w
  LEFT JOIN planeten AS p ON w.id = p.world_id
 WHERE w.typ = 'p'
   AND p.besitzer IS NULL
   AND NOT EXISTS (SELECT 1
                     FROM world w2
                    WHERE w2.x BETWEEN w.x-1 AND w.x+1
                      AND w2.y BETWEEN w.y-1 AND w.y+1
                      AND w2.id <> w.id
                      AND w2.typ IN ('s','p'))

hoffe es hilft,

Markus
 
Es hilft ;) Leider nicht schnell genug:

Code:
SELECT w.x, w.y
FROM world AS w
LEFT JOIN planeten AS p ON w.id = p.world_id
WHERE w.typ = 'p'
AND p.besitzer IS NULL
AND NOT
EXISTS (

SELECT 1
FROM world w2
WHERE w2.x
BETWEEN w.x -1
AND w.x +1
AND w2.y
BETWEEN w.y -1
AND w.y +1
AND w2.id <> w.id
AND w2.typ
IN (
's', 'p'
)
)
LIMIT 1

Zeige Datensätze 0 - 0 (1 insgesamt, die Abfrage dauerte 3.3943 sek.)



Habe mal von jeden Table ein Bild gemacht, und vom Query ein EXPLAIN. Vieleicht könnt ihr was damit anfangen. :-/

http://img5.myimg.de/problem5de.jpg
 
Hallo nochmal,

Bei mir bekomme ich mit 300x300 world mit 35 Schiffen immer Zeiten unter 0,1 Sekunden, bei mySQL wie auch bei Oracle.

Trotzdem kann ich dir möglicherweise folgendes empfehlen:

SQL:
CREATE INDEX idx_worldtyp ON world(typ)

Und dann ein leicht angepasstes Statement. Warum LEFT-Join, wenn du doch weisst anhand des typs "p" dass ein Planet an der Stelle existiert? das führt u.U. zu viel zu vielen Datensätzen, die du dann unnötig mit LIMIT begrenzen musst, obwohl du (wenn ich dich richtig verstanden habe) nur wissen möchtest, ob an Planetenposition (x,y) sich kein anderer Planet und kein Schiff im Umkreis befindet.

Erkläre mal deinen Ablauf, wenn es anders sein sollte.

Wenn der Planet noch nicht wirklich vorhanden ist und du wissen willst, ob an einer ganz konkreten Stelle Platz ist, brauchst du auch nicht der Planetentabelle joinen.

SQL:
SELECT w.x, w.y  
  FROM world AS w  
 INNER JOIN planeten AS p ON w.id = p.world_id 
 WHERE w.typ = 'p'   
   AND p.besitzer IS NULL   
   AND NOT EXISTS (SELECT 1 
                     FROM world w2                    
                    WHERE w2.typ IN ( 's' ,'p' )
                      AND w2.x BETWEEN w.x-1 AND w.x+1
                      AND w2.y BETWEEN w.y-1 AND w.y+1)

So sieht der Plan bei mir aus:
Code:
+----+--------------------+-------+--------+---------------+--------------+---------+--------------+------+-------------+
| id | select_type        | table | type   | possible_keys | key          | key_len | ref          | rows | Extra       |
+----+--------------------+-------+--------+---------------+--------------+---------+--------------+------+-------------+
|  1 | PRIMARY            | p     | ALL    | NULL          | NULL         | NULL    | NULL         |    1 | Using where |
|  1 | PRIMARY            | w     | eq_ref | PRIMARY       | PRIMARY      | 3       | test.p.world |    1 | Using where |
|  2 | DEPENDENT SUBQUERY | w2    | range  | idx_worldtyp  | idx_worldtyp | 2       | NULL         |  555 | Using where |
+----+--------------------+-------+--------+---------------+--------------+---------+--------------+------+-------------+
3 rows in set (0.00 sec)

Markus
 
Ich habe den Query mal bei mir ausgeführt, und leider gibt es nichts zurück komischerweise. Ich weiß nicht warum :-/

MySQL lieferte ein leeres Resultat zurück (d. h. null Zeilen). (die Abfrage dauerte 0.0030 sek.)

Woran könnte das liegen?

PS:
Habe den Index mal drauf gemacht, jedoch dauert nun die andere Abfrage noch länger.
(Zeige Datensätze 0 - 0 (1 insgesamt, die Abfrage dauerte 4.7551 sek.))
 
Hallo,

ich hab auch keine Idee im Moment, da ich ein komplett anderes Verhalten habe hier. Welche MySQL-Version setzt Du ein? Welche Storage-Engine? Und erkläre bitte nochmal den Ablauf, wie du die Daten einfügst und welche Querys du nacheinander ausführst. Vielleicht kann ich Dir dann noch besser weiter helfen.

Poste am besten mal den Inhalt der Datenbank oder schick es mir per Mail: lmarkus30@bluewin.ch.

Markus
 
MySQL: 5.0.41
Engine: MyISAM

Zum Einfügen soll erst mal geschaut werden wo was frei ist. Am besten in der nähe eines Planeten. Jedoch sollen dann nur die Einträge ausgegeben werden, wo sich kein anderer Planet, oder kein anderes Schiff aufhält.

Wenn dies gemacht ist, schreibt er unter 'world' bei dem X und den Y die rausgefundene Position. Als Zusatz kommt bei Typ ein 's' alias Schiff hin. Bei Pic könnte eine 1 hin. Diese spielt nur zur vereinfachten Anzeige der Karte eine Rolle.
Nun schaut er die eingefügte ID, diese bekommt man ja mitgeteilt, und macht einen neuen Eintrag unter 'space'. ID wird automatiosch generiert, 'world_id' bekommt er von dem Eintrag in das Table 'world'. 'ship' wird 1 sein, also ein Standard-Schiff. Dazu gibt es noch ein Table wo die Schiffe drin stehen.
Bei 'besitzer' da kommt die ID des Besitzers rein. Bei 'name' kann der Spieler später seinen Schiffen Namen geben. 'lock' ist dafür zuständig, ob das Schiff überhaupt bewegt werden darf.

Das mit der Datenbank ist so ein Problem :-/ Ich habe viele "Schrott" / "Testeinträge" drin. Das Table 'world' könnte maximal 1.000.000 Einträge fassen. Und zwar 1000x1000. Davon sind ca. 8% Planeten. Das Table wird mit PHP gefüllt.

Soll ich die DB noch mal leeren, und mal füllen lassen und dir schicken? Ich hänge hier einfach mal die Struktur an:

SQL:
-- phpMyAdmin SQL Dump
-- version 2.10.1
-- http://www.phpmyadmin.net
-- 
-- Host: localhost
-- Erstellungszeit: 20. Juli 2007 um 16:13
-- Server Version: 5.0.41
-- PHP-Version: 5.2.2

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";

-- 
-- Datenbank: `web1`
-- 

-- --------------------------------------------------------

-- 
-- Tabellenstruktur für Tabelle `members`
-- 

CREATE TABLE `members` (
  `id` smallint(5) unsigned NOT NULL auto_increment,
  `nickname` varchar(20) collate latin1_general_ci NOT NULL,
  `passwort` varchar(40) collate latin1_general_ci NOT NULL,
  `code` varchar(10) collate latin1_general_ci NOT NULL,
  `email` varchar(30) collate latin1_general_ci NOT NULL,
  `l_planet` smallint(5) unsigned NOT NULL,
  `angemeldet` int(10) unsigned NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;

-- --------------------------------------------------------

-- 
-- Tabellenstruktur für Tabelle `planeten`
-- 

CREATE TABLE `planeten` (
  `id` mediumint(6) unsigned NOT NULL auto_increment,
  `world_id` mediumint(7) unsigned NOT NULL,
  `besitzer` mediumint(7) unsigned NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;

-- --------------------------------------------------------

-- 
-- Tabellenstruktur für Tabelle `space`
-- 

CREATE TABLE `space` (
  `id` mediumint(12) NOT NULL auto_increment,
  `world_id` mediumint(7) NOT NULL,
  `ship` smallint(2) NOT NULL,
  `besitzer` mediumint(7) NOT NULL,
  `name` varchar(20) collate latin1_general_ci NOT NULL,
  `lock` tinyint(1) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;

-- --------------------------------------------------------

-- 
-- Tabellenstruktur für Tabelle `space_ships`
-- 

CREATE TABLE `space_ships` (
  `id` smallint(2) unsigned NOT NULL auto_increment,
  `name` varchar(20) collate latin1_general_ci NOT NULL,
  `max_speed` smallint(4) unsigned NOT NULL,
  `max_see` smallint(2) unsigned NOT NULL,
  `max_angriff` smallint(4) unsigned NOT NULL,
  `max_verteidigung` smallint(4) unsigned NOT NULL,
  `max_rohstoffe` mediumint(7) unsigned NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;

-- --------------------------------------------------------

-- 
-- Tabellenstruktur für Tabelle `world`
-- 

CREATE TABLE `world` (
  `id` mediumint(7) unsigned NOT NULL auto_increment,
  `x` smallint(3) unsigned NOT NULL,
  `y` smallint(3) unsigned NOT NULL,
  `typ` char(1) collate latin1_general_ci NOT NULL,
  `pic` smallint(2) unsigned NOT NULL,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `xy` (`x`,`y`),
  KEY `idx_worldtyp` (`typ`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
 
Ja schicks mir mal gefüllt.

Ich bin nicht von 8% Planeten ausgegangen ;) Laut deinem phpMyAdmin Screenshot sah das anders aus ;-)

Schicks mir einfach mal zu. Dann schau ich was ich machen kann

Markus
 
Hallo nochmal,

Aha! Das war wohl ein Missverständnis ;-) Du hast ja doch nur Einträge in deiner "world"-Tabelle, wo sich entweder ein Planet oder ein Schiff befindet.
Denn davon ist meine erste Lösung _nicht_ ausgegangen - klar dass sie dann für Dich nicht funktioniert.

Du hast ja nun keine Rows in der Tabelle "world" für die freien Spots, daher wird es aber mit den von MySQL standardmässig bereitgestellten Mitteln schwierig bis unmöglich eine freie Position nur mit SQL zu finden. Window-Funktionen gibt es nicht, Geo-Funktionen gibt es zwar, hab ich jetzt aber mal aussen vorgelassen. Sicher gibt es auch Möglichkeiten durch diverse Self-Joins zu einer Lösung zu kommen, aber ich hab mal hier mal einen anderen Ansatz gewählt:

Erst der Source, später die Erklärung.

SQL:
DELIMITER $$

CREATE FUNCTION getEmptySpot(Abstand INTEGER(3)) RETURNS INTEGER(10)
BEGIN
  -- Zufallswerte für X und Y
  DECLARE randX INTEGER(4) DEFAULT 0;
  DECLARE randY INTEGER(4) DEFAULT 0;

  -- maximale Ausdehnung der Welt
  DECLARE maxX INTEGER(4);
  DECLARE maxY INTEGER(4);

  -- zaehlt wieviele Objekte in Umgebung vorhanden
  DECLARE anzUmgebung INTEGER(2) DEFAULT 0;

  -- Sicherheits-Zaehler, falls gar nichts gefunden wird
  DECLARE anzDurchlaeufe INTEGER(3) DEFAULT 0;

  -- Flag, ob freier Spot gefunden wurde
  DECLARE gefunden BOOLEAN DEFAULT false;
  DECLARE fertig   BOOLEAN DEFAULT false;

  -- Ausdehnung der Welt ermitteln
  SELECT MAX(x), MAX(y)
    INTO maxX, maxY
    FROM world;

  -- solange suchen, bis ein freier Spot gefunden
  REPEAT

    -- hole eine neue zufaellige Position
    SET randX = FLOOR(RAND() * maxX) + 1;
    SET randY = FLOOR(RAND() * maxY) + 1;

    -- Anzahl Objekte in Umgebung zaehlen
    SELECT count(*)
      INTO anzUmgebung
      FROM world
     WHERE (typ ='s' OR typ='p')
       AND x BETWEEN randX-Abstand AND randX+Abstand
       AND y BETWEEN randY-Abstand AND randY+Abstand;

      -- pruefen ob Umgebung frei ist
    IF anzUmgebung = 0 THEN
      SET gefunden = true;
      SET fertig = true;
    END IF;

    SET anzDurchlaeufe = anzDurchlaeufe + 1;

  UNTIL (gefunden = true) OR (anzDurchlaeufe = 500)
  END REPEAT;

  -- Gib Position zurueck
  IF fertig THEN
    RETURN randX*10000 + randY;
  ELSE
    RETURN 0;
  END IF;

END $$

DELIMITER ;

Hierbei versuche ich mittels Zufallszahlen eine neue Position zu ermitteln. Dann prüfe ich, ob es in der Umgebung schon Objekte gibt. Wenn ja, generiere ich neue Zufallszahlen und versuche es an einer der neuen Position noch einmal usw. Wenn ich eine Position gefunden habe, bei der keinerlei Objekte in der Umgebung vorhanden sind, breche ich ab und liefere die x- und y-Positonen zurück.

Anmerkung 1) Die Position wird zurückgegeben als INTEGER, der sowohl die X- als auch Y-Koordinate enthält. Dieser setzt sich zusammen aus der X-Koordinate * 10000 addiert zu der Y-Koordinate.
Falls keine freie Position gefunden wurde, wird 0 zurück gegeben.

Um daraus wieder an die X- und Y-Koordinate zu kommen, rechnest du z.B. so:

Code:
X = POS div 10000 (Ganzzahlige Integerdivision)
Y = POS mod 10000 (Modulo, d.h. Rest der Division)

Du kannst das ganze natürlich umschreiben, dass die Positonen als OUT-Parameter zurück kommen, oder als String, den du dann parst. Wie auch immer.

Anmerkung 2) Du hast die Möglichkeit, einen Parameter "Abstand" mitzugeben, der den Bereich angibt, der um die Position frei sein soll. Je grösser der Bereich, desto länger läuft die Funktion, da mehr Iterationen durchgeführt werden müssen um eine passende Position zu finden. Daher...

Anmerkung 3) ... muss die Anzahl Durchläufe begrenzt werden, falls der "Abstand" zu gross gewählt wurde oder tatsächlich nichts mehr frei sein sollte. Ich hab jetzt mal 500 gewählt, das sollte für den Normalfall reichen.

Anmerkung 4) Es kann natürlich aufgrund des Zufallszahlengenerators bei grösser gewählten "Abständen" oder weil man einfach Pech hat, dazu kommen, dass man keine freie Position erhält, trotz dass noch eine vorhanden wäre. Da solltest du schauen, dass du entweder die Funktion nochmal ausführst oder aber abbrichst.

Testen kannst du die Funktion z.B. mittels

SQL:
SELECT getEmptySpot(1)

Wie du weiter verfährst, bleibt Dir überlassen. Du kannst noch eine Stored Procedure schreiben, die die eigentliche Arbeit macht und den Planet bzw. das Schiff anlegt und den Eintrag in der "world"-Tabelle vornimmt.
Du kannst das ganze dann natürlich auch in deiner Host-Umgebung machen (PHP etc.)

Hoffe diesmal hilft es, ;-)

Markus

Edit: Was noch nicht optimal ist, ist die Ermittlung der Ausdehnung der Welt. Entweder legst du sie fest, gibst sie als Parameter mit, oder hast eine zusätzliche Tabelle in der DB, in der diese Information gespeichert ist.
 
Zuletzt bearbeitet:

Neue Beiträge

Zurück